Skip to content

[Bug] Hexagon codegen is broken on main and the only test covering it never runs #20352

Description

@neuregex

Expected behavior

tests/python/codegen/test_target_codegen_hexagon.py compiles three small modules for
qcom/hexagon-v66 and inspects the generated assembly and LLVM IR. It registers a phony linker
(/bin/true) with the comment "so that we can test codegen without a Hexagon toolchain", so I
expected it to pass on any build whose LLVM has the Hexagon target — no SDK, no device.

Actual behavior

Those three tests never run, and they cannot pass on main.

They are gated on env.has_hexagon(), which requires both HEXAGON_TOOLCHAIN and
ANDROID_SERIAL_NUMBER (an attached device), so they are always skipped — there is no hexagon
mention left under ci/ or .github/. The Hexagon codegen is still compiled into every
LLVM-enabled build though: src/backend/hexagon/codegen/llvm/*.cc is in COMPILER_LLVM_SRCS
next to the CUDA and ROCm LLVM codegen (cmake/modules/LLVM.cmake:62), independent of
USE_HEXAGON, which only controls the device runtime. So it gets recompiled and carried through
refactors on every build, with no test exercising it.

When I narrow the gate so the tests run on a plain USE_LLVM build (USE_HEXAGON=OFF, no
toolchain, no device), all three fail, for two independent reasons.

1. unknown intrinsic tirx.tvm_call_packed

src/target/llvm/codegen_llvm.cc:1545: in CodeGenLLVM::CreateIntrinsic
E   tvm.error.InternalError: unknown intrinsic ir.Op(... name="tirx.tvm_call_packed" ...)

split_host_device_mods decides host-ness by target kind (python/tvm/tirx/build.py:104):

def is_host_func(f):
    target = f.attrs.get("target", tvm.target.Target("llvm"))
    return target.kind.name in ["llvm", "c"]

Its own docstring says something else: "Functions with cpu in the target string are considered
host functions"
. The hexagon kind registers set_default_keys({"hexagon", "cpu"})
(src/backend/hexagon/codegen/target_kind.cc:42), so with Target(target, target) — target and
host both hexagon, which is what the test does — the host function is classified as a device
function. Device modules only get finalize_device_passes(), which does not include
LowerTVMBuiltin(). So the tvm_call_packed that MakePackedAPI inserts for the set_device
call (src/tirx/transform/make_packed_api.cc:261) is never lowered to tvm_call_packed_lowered,
the form CodeGenCPU knows how to handle.

Only llvm, c and hexagon register the cpu key, so this gap affects exactly one target kind.

2. Cannot open /tmp/tvm-XXXXXX.so

With (1) worked around locally, the failure moves to:

<unknown>:0: in tvm::codegen::BuildHexagon(tvm::IRModule, tvm::Target)
src/runtime/file_utils.cc:83: in LoadBinaryFromFile
E   tvm.error.InternalError: Check failed: (!fs.fail()) is false: Cannot open /tmp/tvm-52eea3.so

BuildHexagon emits asm, obj, IR and bitcode into strings, then calls
tvm.contrib.hexagon.link_shared and reads the linked .so back from disk:

std::string so_data;
runtime::LoadBinaryFromFile(so_name, &so_data);
return target::HexagonModuleCreateWithFallback(ffi::Bytes(std::move(so_data)), ...);

Before #19757 (2026-06-13) that tail was return HexagonModuleCreate(so_name, "so", ...): the
filename was passed along and never opened, which is what made the phony-linker approach work.
With the read-back, codegen-only use needs a real Hexagon linker, even though every artifact the
test inspects has already been produced by that point. #19796 (2026-06-16) removed the Hexagon app
and test wrappers three days later, so nothing was left that could have caught it.

Environment

  • main at 87b5b27dac, also reproduced at cc0f9f07c1
  • Ubuntu 24.04 (WSL2), LLVM 18.1.3 via llvm-config-18 --ignore-libllvm --link-static,
    CMake 3.28.3, Python 3.12
  • USE_HEXAGON=OFF, no Hexagon SDK, no device, HEXAGON_TOOLCHAIN and ANDROID_SERIAL_NUMBER
    unset

Steps to reproduce

Build with USE_LLVM pointing at any LLVM that lists Hexagon in llvm-config --targets-built
(the stock Ubuntu llvm-18-dev does), then:

unset HEXAGON_TOOLCHAIN ANDROID_SERIAL_NUMBER
pytest tests/python/codegen/test_target_codegen_hexagon.py -v
# 3 skipped: need hexagon

Add a narrower probe in python/tvm/testing/env.py and gate the three tests with it instead of
has_hexagon():

@functools.cache
def has_hexagon_codegen() -> bool:
    if not has_llvm():
        return False
    try:
        return any(str(t).lower() == "hexagon" for t in tvm.target.codegen.llvm_get_targets())
    except Exception:  # pylint: disable=broad-except
        return False
# 3 failed: unknown intrinsic tirx.tvm_call_packed

Then, for the second failure, widen is_host_func to honour the cpu key:

return target.kind.name in ["llvm", "c"] or "cpu" in target.keys
# 3 failed: Cannot open /tmp/tvm-XXXXXX.so

That one-liner is regression-clean here — tests/python/{codegen,tirx,driver,target,testing} gives
1605 passed, 3195 skipped, nothing newly failing — but I don't think it is the right fix: it would
also send a Hexagon device kernel to the host module in the usual cross-compilation setup
(target=hexagon, host=llvm), and a split on calling_conv looks closer to what the docstring
describes.

Questions before sending a PR

Both fixes look like design calls rather than obvious patches:

  1. Should split_host_device_mods key on calling_conv (or on the host target) instead of the
    target kind?
  2. Should BuildHexagon keep codegen usable without a linker — loading the .so only when one was
    actually produced — or should the test stop going through tvm.compile to inspect generated
    code?

I'm happy to send a PR for whichever shape you prefer, together with the test-gate change so this
is covered by CI from then on. My repro needs no device and no SDK, just an LLVM with the Hexagon
target.

Triage

  • needs-triage

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions