MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
ll_nvptx.cpp
Go to the documentation of this file.
2
3#include <fe/sys.h>
4
5#include <mim/driver.h>
6#include <mim/plugin.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 = fe::sys::require_cmd("nvidia-smi");
38 auto out = fe::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 = fe::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<fe::sys::CmdNotFound>(
109 MIM_LL_NVPTX_BE "unable to find `{}`; try setting the `CUDA_HOME` environment variable", Libdevice_Name);
110}
111
112void link_libdevice(const NvptxCompileArgs& c) {
113 if (!std::filesystem::exists(c.libdevice_path))
114 fe::throwf(MIM_LL_NVPTX_BE "libdevice path does not exist: `{}`", c.libdevice_path);
115 auto llvm_link = fe::sys::require_cmd("llvm-link");
116 fe::sys::require_run(std::format("{} {} {} {} -o {}", llvm_link, c.link_llvm_args, c.dev_ll_name, c.libdevice_path,
117 c.dev_bc_raw_name));
118}
119
120void optimize_bytecode(const NvptxCompileArgs& c) {
121 auto opt = fe::sys::require_cmd("opt");
122 fe::sys::require_run(std::format("{} {} {} -o {}", opt, c.opt_args, c.dev_bc_raw_name, c.dev_bc_opt_name));
123}
124
125void compile2ptx(const NvptxCompileArgs& c, bool uses_libdevice) {
126 auto compile_input = uses_libdevice ? c.dev_bc_opt_name : c.dev_ll_name;
127 auto llc = fe::sys::require_cmd("llc");
128 fe::sys::require_run(std::format("{} -march=nvptx64 -mcpu=sm_{} {} {} -o {}", llc, c.compute_cap, c.llc_args,
129 compile_input, c.dev_ptx_name));
130}
131
132void compile2cubin(const NvptxCompileArgs& c) {
133 auto ptxas = fe::sys::require_cmd("ptxas");
134 fe::sys::require_run(std::format("{} -arch=sm_{} {} {} -o {}", ptxas, c.compute_cap, c.ptxas_args, c.dev_ptx_name,
135 c.dev_cubin_name));
136}
137
138void compile2fatbin(const NvptxCompileArgs& c) {
139 auto fatbinary = fe::sys::require_cmd("fatbinary");
140 auto ptx_args = ""s;
141 if (c.embed_ptx) {
142 ptx_args = std::format("--image3=kind=ptx,sm={},file={}", c.compute_cap, c.dev_ptx_name);
143 if (!c.ptxas_args.empty()) ptx_args += std::format(" --cmdline={}", c.ptxas_args);
144 }
145 auto cubin_args = ""s;
146 if (c.embed_cubin) cubin_args = std::format("--image3=kind=elf,sm={},file={}", c.compute_cap, c.dev_cubin_name);
147 fe::sys::require_run(std::format("{} --create={} -64 {} {} {}", fatbinary, c.dev_fatbin_name, c.fatbinary_args,
148 ptx_args, cubin_args));
149}
150
151} // namespace
152
153class Emit : public Phase {
154public:
157
158 void start() override {
159 auto name = world().name() ? world().name().str() : "a"s;
160
161 auto c = NvptxCompileArgs{};
162 c.host_ll_name = name + ".ll"s;
163 c.dev_ll_name = name + "_dev.ll"s;
164 c.dev_ptx_name = name + "_dev.ptx"s;
165 c.dev_cubin_name = name + "_dev.cubin"s;
166 c.dev_fatbin_name = name + "_dev.fatbin"s;
167 c.dev_bc_raw_name = name + "_dev_raw.bc"s;
168 c.dev_bc_opt_name = name + "_dev_opt.bc"s;
169
170 world().log().d("ll_nvptx backend args: {}", fe::Join(args()));
171
172 // clang-format off
173 if (auto v = arg_value(args(), "o", "output")) c.host_ll_name = *v;
174 if (auto v = arg_value(args(), "o-dev", "output-dev")) c.dev_ll_name = *v;
175 if (auto v = arg_value(args(), "sm")) c.compute_cap = *v;
176 if (auto v = arg_value(args(), "libdevice")) c.libdevice_path = *v;
177 if (auto v = arg_value(args(), "Xlink_llvm")) c.link_llvm_args = *v;
178 if (auto v = arg_value(args(), "Xopt")) c.opt_args = *v;
179 if (auto v = arg_value(args(), "Xllc")) c.llc_args = *v;
180 if (auto v = arg_value(args(), "Xptxas")) c.ptxas_args = *v;
181 if (auto v = arg_value(args(), "Xfatbinary")) c.fatbinary_args = *v;
182 if (auto b = arg_bool(args(), {"embed"}, {"no-embed"})) c.embed_device_code = *b;
183 if (arg_flag(args(), "no-ptx-embed")) c.embed_ptx = false;
184 if (arg_flag(args(), "no-cubin-embed")) c.embed_cubin = false;
185 // clang-format on
186
187 auto rt = arg_value(args(), "rt") == "extern" ? ll::Emitter::Rt::ext : ll::Emitter::Rt::embed;
188
189 auto split_apply_phase = Phase::create(world().driver().phases(), world().annex<gpu::split_apply>());
190 auto setup_phase
191 = split_apply_phase.get()->expect<RWPhase>("the phase for `gpu.split_apply` to be an `RWPhase`");
192 setup_phase->run();
193
194 DeviceEmitFlags device_flags;
195 {
196 auto dev_out = Out(c.dev_ll_name);
197 device_flags = emit_device(setup_phase->new_world(), *dev_out.os());
198 }
199 if (c.embed_device_code) {
200 if (!c.embed_ptx && !c.embed_cubin)
201 fe::throwf(MIM_LL_NVPTX_BE "embedding requested with no images (neither PTX nor CUBIN)");
202 try {
203 if (c.compute_cap.empty()) c.compute_cap = get_compute_capability();
204 if (device_flags.uses_libdevice) {
205 if (c.libdevice_path.empty()) c.libdevice_path = find_libdevice();
206 link_libdevice(c);
207 optimize_bytecode(c);
208 }
209 compile2ptx(c, device_flags.uses_libdevice);
210 compile2cubin(c);
211 compile2fatbin(c);
212 } catch (const fe::sys::CmdNotFound& e) {
213 log().w("{}; not embedding device code", e.what());
214 c.embed_device_code = false;
215 }
216 }
217 auto device_fatbin_file = c.embed_device_code ? std::optional(c.dev_fatbin_name) : std::nullopt;
218 auto host_out = Out(c.host_ll_name);
219 emit_host(setup_phase->old_world(), *host_out.os(), device_fatbin_file, rt);
220
221 if (c.embed_device_code) {
222 std::println(std::cout, "Unified (Fat) LLVM IR written to {}", c.host_ll_name);
223 } else {
224 std::println(std::cout, "Host-only LLVM IR written to {}", c.host_ll_name);
225 std::println(std::cout, "Device-only LLVM IR written to {}", c.dev_ll_name);
226 }
227 }
228};
229
230} // namespace mim::plug::ll_nvptx
231
232using namespace mim;
233
235
236// clang-format off
237static constexpr PluginArg known_args[] = {
238 {"o=<file>, output=<file>", "Writes the host LLVM IR to `<file>` instead of the default `<world>.ll`/`a.ll`; `<file>` may be `-` for stdout."},
239 {"o-dev=<file>, output-dev=<file>", "Writes the device LLVM IR to `<file>` instead of the default `<world>_dev.ll`/`a_dev.ll`; `<file>` may be `-` for stdout."},
240 {"rt=embed, rt=extern", "Like `ll`'s `rt`, but for the host module's C [runtime wrappers](@ref plugin_runtime) such as `@mim_cu_check`."},
241 {"embed, no-embed", "Embeds the compiled device binary into the host LLVM IR, or doesn't; the default is `embed` on Linux and `no-embed` elsewhere."},
242 {"no-ptx-embed", "When embedding: omits the PTX image from the fat binary (default: both PTX and CUBIN)."},
243 {"no-cubin-embed", "When embedding: omits the CUBIN image from the fat binary (default: both PTX and CUBIN)."},
244 {"sm=<SM>", "When embedding: compiles the device binary for compute capability `sm_<SM>`."},
245 {"libdevice=<path>", "When embedding and linking libdevice: uses the NVVM library at `<path>` instead of locating it via the CUDA paths."},
246 {"Xlink_llvm=<args>", "When embedding and linking libdevice: passes `<args>` to `link_llvm` (default: none)."},
247 {"Xopt=<args>", "When embedding and linking libdevice: passes `<args>` to `opt` (default: `-passes=\"default<O2>,nvvm-reflect\"`)."},
248 {"Xllc=<args>", "When embedding: passes `<args>` to `llc` (default: none)."},
249 {"Xptxas=<args>", "When embedding: passes `<args>` to `ptxas` (default: none); also passed to `fatbinary` via `--cmdline` when the PTX image is embedded."},
250 {"Xfatbinary=<args>", "When embedding: passes `<args>` to `fatbinary` (default: none)."},
251};
252
253static constexpr PluginEnv known_envs[] = {
254 {"CUDA_HOME", "Root of the CUDA installation to locate `libdevice` in, if the CUDA paths do not yield one."},
255};
256// clang-format on
257
259 return {"ll_nvptx", MIM_VERSION, {}, reg_phases,
260 known_args, std::size(known_args), known_envs, std::size(known_envs)};
261}
void reg_phases(Flags2Phases &phases)
Definition affine.cpp:12
Out()=default
static void hook(Flags2Phases &phases)
Definition phase.h:70
flags_t annex() const
Definition phase.h:81
const fe::Log & log() const
Definition phase.h:79
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
const fe::Vector< std::string > & args()
Command-line arguments passed to this Phase's plugin via -X <plugin>:<arg>.
Definition phase.cpp:23
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
Rebuilds old_world() into new_world() and then swaps them.
Definition phase.h:427
The World represents the whole program and manages creation of MimIR nodes (Defs).
Definition world.h:40
Sym name() const
Definition world.h:109
const fe::Log & log() const
Log via log().e("...", args) etc.; owned by the Driver.
Definition world.cpp:129
@ embed
Splice the wrapper IR into the emitted module so it is self-contained.
Definition ll.h:166
@ ext
Only declare the wrappers; the runtime is linked in externally.
Definition ll.h:167
void start() override
Actual entry.
Definition ll_nvptx.cpp:158
Emit(World &world, flags_t annex)
Definition ll_nvptx.cpp:155
#define MIM_EXPORT
Definition config.h:21
static constexpr PluginEnv known_envs[]
Definition ll_nvptx.cpp:253
static constexpr PluginArg known_args[]
Definition ll_nvptx.cpp:237
The ll_nvptx Plugin
Definition ll_nvptx.h:12
DeviceEmitFlags emit_device(World &, std::ostream &)
Definition ll_nvptx.cpp:664
void emit_host(World &, std::ostream &, std::optional< std::string >, ll::Emitter::Rt rt=ll::Emitter::Rt::embed)
Definition ll_nvptx.cpp:656
Definition ast.h:16
bool arg_flag(fe::View< std::string > args, Keys... keys)
Whether any of keys occurs.
Definition plugin.h:88
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:30
mim::Plugin mim_get_plugin()
std::optional< bool > arg_bool(fe::View< std::string > args, std::initializer_list< std::string_view > on, std::initializer_list< std::string_view > off)
An on key ↦ true, an off key ↦ false; std::nullopt if neither occurs.
Definition plugin.h:73
std::optional< std::string_view > arg_value(fe::View< std::string > args, Keys... keys)
Value of <key>=<value>; std::nullopt if none of keys carries one.
Definition plugin.h:64
One -X <plugin>:<arg> a Plugin understands; see Arguments.
Definition plugin.h:34
One environment variable a Plugin reads; see Environment Variables.
Definition plugin.h:41
#define MIM_LL_NVPTX_BE
Prefix for this backend's fe::throwf messages; see MIM_LL_BE.
Definition ll_nvptx.h:15
#define MIM_VERSION
Definition plugin.h:149
Basic info and registration function pointer to be returned from a specific plugin.
Definition plugin.h:154