Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,31 @@ def test_matches_over_every_short_separator_string(self):
LLVM_BIN_ENV = "ROCKE_TEST_LLVM_BIN"


def configured_llvm_dirs() -> list[str]:
"""List explicitly configured LLVM directories in precedence order."""
configured = []
if os.environ.get(LLVM_BIN_ENV):
configured.append(os.environ[LLVM_BIN_ENV])
if os.environ.get("ROCM_PATH"):
configured.extend(
os.path.join(os.environ["ROCM_PATH"], suffix)
for suffix in (
os.path.join("llvm", "bin"),
os.path.join("lib", "llvm", "bin"),
)
)
return configured


def configured_llvm_tool(name):
"""Find ``name`` only in an explicitly configured LLVM toolchain."""
for directory in configured_llvm_dirs():
candidate = shutil.which(name, path=directory)
if candidate:
return candidate
return None


def llvm_tool(name):
"""An LLVM tool from the configured toolchain, PATH, or the ROCm default.

Expand All @@ -780,21 +805,63 @@ def llvm_tool(name):
unrunnable file, so the route this prefers was skipped on Windows and the
system tool answered in its place -- the exact substitution above.
"""
configured = []
if os.environ.get(LLVM_BIN_ENV):
configured.append(os.environ[LLVM_BIN_ENV])
if os.environ.get("ROCM_PATH"):
configured.append(os.path.join(os.environ["ROCM_PATH"], "llvm", "bin"))
for directory in configured:
candidate = shutil.which(name, path=directory)
if candidate:
return candidate
candidate = configured_llvm_tool(name)
if candidate:
return candidate
found = shutil.which(name)
if found:
return found
return shutil.which(name, path=os.path.join("/opt/rocm", "llvm", "bin"))


def llvm_codegen_tool():
"""Return an AMDGPU object-code driver respecting directory precedence.

Some ROCm packages ship ``clang`` but not ``llc``. Try llc then Clang in
each configured directory before moving to the next, so a lower-priority
llc cannot displace a higher-priority Clang. If no configured driver is
available, retain the existing PATH/default fallback.
"""
for directory in configured_llvm_dirs():
for name in ("llc", "clang"):
candidate = shutil.which(name, path=directory)
if candidate:
return name, candidate
for name in ("llc", "clang"):
candidate = llvm_tool(name)
if candidate:
return name, candidate
return None, None


def amdgpu_object_command(driver, tool, bitcode, output, arch="gfx950"):
"""Build the equivalent llc or clang command for one AMDGPU object."""
if driver == "llc":
return [
tool,
"-mtriple=amdgcn-amd-amdhsa",
f"-mcpu={arch}",
"-filetype=obj",
bitcode,
"-o",
output,
]
if driver == "clang":
# Let the .bc suffix select LLVM bitcode input. Passing ``-x ir`` makes
# clang treat this as frontend input and loses the inline DWARF chain.
return [
tool,
"--target=amdgcn-amd-amdhsa",
f"-mcpu={arch}",
"-nogpulib",
"-c",
bitcode,
"-o",
output,
]
raise ValueError(f"unsupported LLVM codegen driver: {driver!r}")


class TestLlvmToolDiscovery(unittest.TestCase):
"""Which ``llc`` answers decides whether the round trip below means anything.

Expand Down Expand Up @@ -881,6 +948,102 @@ def test_a_configured_root_without_the_tool_falls_through(self):
elif found is not None:
self.assertTrue(os.path.isfile(found))

def test_lib_llvm_bin_layout_is_searched(self):
with tempfile.TemporaryDirectory() as tmp:
expected = self.make_tool(os.path.join(tmp, "lib", "llvm", "bin"), "llc")
with mock.patch.dict(os.environ, {"ROCM_PATH": tmp}) as env:
env.pop(LLVM_BIN_ENV, None)
self.assertSameTool(llvm_tool("llc"), expected)

def test_explicit_bin_clang_beats_rocm_llc(self):
with tempfile.TemporaryDirectory() as tmp:
override = os.path.join(tmp, "override")
expected = self.make_tool(override, "clang")
assembler = self.make_tool(override, "llvm-as")
dumper = self.make_tool(override, "llvm-dwarfdump")
self.fake_bin(tmp, "llc")
with mock.patch.dict(
os.environ, {"ROCM_PATH": tmp, LLVM_BIN_ENV: override}
):
self.assertSameTool(llvm_tool("llvm-as"), assembler)
self.assertSameTool(llvm_tool("llvm-dwarfdump"), dumper)
driver, tool = llvm_codegen_tool()
self.assertEqual(driver, "clang")
self.assertSameTool(tool, expected)

def test_rocm_llvm_bin_clang_beats_lib_llvm_bin_llc(self):
with tempfile.TemporaryDirectory() as tmp:
expected = self.fake_bin(tmp, "clang")
self.make_tool(os.path.join(tmp, "lib", "llvm", "bin"), "llc")
with mock.patch.dict(os.environ, {"ROCM_PATH": tmp}) as env:
env.pop(LLVM_BIN_ENV, None)
driver, tool = llvm_codegen_tool()
self.assertEqual(driver, "clang")
self.assertSameTool(tool, expected)

def test_codegen_falls_through_configured_bin_without_drivers(self):
with tempfile.TemporaryDirectory() as tmp:
override = os.path.join(tmp, "override")
self.make_tool(override, "llvm-as")
expected = self.fake_bin(tmp, "clang")
with mock.patch.dict(
os.environ, {"ROCM_PATH": tmp, LLVM_BIN_ENV: override}
):
driver, tool = llvm_codegen_tool()
self.assertEqual(driver, "clang")
self.assertSameTool(tool, expected)

def test_llc_wins_over_clang_in_same_configured_bin(self):
with tempfile.TemporaryDirectory() as tmp:
expected = self.fake_bin(tmp, "llc")
self.fake_bin(tmp, "clang")
with mock.patch.dict(os.environ, {"ROCM_PATH": tmp}) as env:
env.pop(LLVM_BIN_ENV, None)
driver, tool = llvm_codegen_tool()
self.assertEqual(driver, "llc")
self.assertSameTool(tool, expected)

def test_configured_clang_beats_unrelated_path_llc(self):
with tempfile.TemporaryDirectory() as tmp:
configured_clang = self.fake_bin(tmp, "clang")
path_dir = os.path.join(tmp, "path")
self.make_tool(path_dir, "llc")
with mock.patch.dict(
os.environ,
{"ROCM_PATH": tmp, "PATH": path_dir},
) as env:
env.pop(LLVM_BIN_ENV, None)
driver, tool = llvm_codegen_tool()
self.assertEqual(driver, "clang")
self.assertSameTool(tool, configured_clang)

def test_codegen_commands_are_equivalent(self):
self.assertEqual(
amdgpu_object_command("llc", "llc", "k.bc", "k.o"),
[
"llc",
"-mtriple=amdgcn-amd-amdhsa",
"-mcpu=gfx950",
"-filetype=obj",
"k.bc",
"-o",
"k.o",
],
)
self.assertEqual(
amdgpu_object_command("clang", "clang", "k.bc", "k.o"),
[
"clang",
"--target=amdgcn-amd-amdhsa",
"-mcpu=gfx950",
"-nogpulib",
"-c",
"k.bc",
"-o",
"k.o",
],
)


class TestObjectRoundTrip(unittest.TestCase):
"""The metadata has to survive into a real code object, not just the IR.
Expand All @@ -894,8 +1057,11 @@ class TestObjectRoundTrip(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.tools = {n: llvm_tool(n) for n in ("llvm-as", "llc", "llvm-dwarfdump")}
cls.tools = {n: llvm_tool(n) for n in ("llvm-as", "llvm-dwarfdump")}
cls.codegen = llvm_codegen_tool()
missing = [n for n, p in cls.tools.items() if p is None]
if cls.codegen[1] is None:
missing.append("llc or clang")
Comment thread
tenpercent marked this conversation as resolved.
Resolved
if missing:
raise unittest.SkipTest(f"missing LLVM tools: {', '.join(missing)}")

Expand All @@ -909,15 +1075,9 @@ def dwarf_for(self, kernel):
[self.tools["llvm-as"], paths["ll"], "-o", paths["bc"]], check=True
)
subprocess.run(
[
self.tools["llc"],
"-mtriple=amdgcn-amd-amdhsa",
"-mcpu=gfx950",
"-filetype=obj",
paths["bc"],
"-o",
paths["o"],
],
amdgpu_object_command(
self.codegen[0], self.codegen[1], paths["bc"], paths["o"]
),
check=True,
)
return subprocess.run(
Expand Down
Loading