12using namespace std::string_literals;
13using namespace std::string_view_literals;
19struct NvptxCompileArgs {
20 std::string host_ll_name, dev_ll_name, dev_ptx_name, dev_cubin_name, dev_fatbin_name, dev_bc_raw_name,
23 bool embed_device_code =
true;
25 bool embed_device_code =
false;
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,
34constexpr auto Default_Compute_Cap =
"75";
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);
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: '{}'.",
46 return Default_Compute_Cap;
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: '{}'.",
54 return Default_Compute_Cap;
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);
63constexpr auto Libdevice_Name =
"libdevice.10.bc"sv;
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;
69 std::ifstream file(profile_path);
70 if (!file.is_open())
return std::nullopt;
72 std::string line, top_dir, lib_dir;
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);
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);
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;
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();
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();
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();
108 fe::throwf<fe::sys::CmdNotFound>(
109 MIM_LL_NVPTX_BE "unable to find `{}`; try setting the `CUDA_HOME` environment variable", Libdevice_Name);
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,
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));
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));
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,
138void compile2fatbin(
const NvptxCompileArgs& c) {
139 auto fatbinary = fe::sys::require_cmd(
"fatbinary");
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);
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));
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;
170 world().
log().d(
"ll_nvptx backend args: {}", fe::Join(
args()));
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;
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;
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;
191 = split_apply_phase.get()->expect<
RWPhase>(
"the phase for `gpu.split_apply` to be an `RWPhase`");
196 auto dev_out =
Out(c.dev_ll_name);
197 device_flags =
emit_device(setup_phase->new_world(), *dev_out.os());
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)");
203 if (c.compute_cap.empty()) c.compute_cap = get_compute_capability();
205 if (c.libdevice_path.empty()) c.libdevice_path = find_libdevice();
207 optimize_bytecode(c);
212 }
catch (
const fe::sys::CmdNotFound& e) {
213 log().w(
"{}; not embedding device code", e.what());
214 c.embed_device_code =
false;
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);
221 if (c.embed_device_code) {
222 std::println(std::cout,
"Unified (Fat) LLVM IR written to {}", c.host_ll_name);
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);
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)."},
254 {
"CUDA_HOME",
"Root of the CUDA installation to locate `libdevice` in, if the CUDA paths do not yield one."},
void reg_phases(Flags2Phases &phases)
static void hook(Flags2Phases &phases)
const fe::Log & log() const
Phase(World &world, std::string name)
static std::unique_ptr< Phase > create(const Flags2Phases &phases, const Def *def)
const fe::Vector< std::string > & args()
Command-line arguments passed to this Phase's plugin via -X <plugin>:<arg>.
virtual void run()
Entry point and generates some debug output; invokes Phase::start.
std::string_view name() const
Rebuilds old_world() into new_world() and then swaps them.
The World represents the whole program and manages creation of MimIR nodes (Defs).
const fe::Log & log() const
Log via log().e("...", args) etc.; owned by the Driver.
@ embed
Splice the wrapper IR into the emitted module so it is self-contained.
@ ext
Only declare the wrappers; the runtime is linked in externally.
void start() override
Actual entry.
Emit(World &world, flags_t annex)
static constexpr PluginEnv known_envs[]
static constexpr PluginArg known_args[]
DeviceEmitFlags emit_device(World &, std::ostream &)
void emit_host(World &, std::ostream &, std::optional< std::string >, ll::Emitter::Rt rt=ll::Emitter::Rt::embed)
bool arg_flag(fe::View< std::string > args, Keys... keys)
Whether any of keys occurs.
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.
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.
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.
One -X <plugin>:<arg> a Plugin understands; see Arguments.
One environment variable a Plugin reads; see Environment Variables.
#define MIM_LL_NVPTX_BE
Prefix for this backend's fe::throwf messages; see MIM_LL_BE.
Basic info and registration function pointer to be returned from a specific plugin.