Skip to content

Serialize TypeTree offsets as signed constants - #3070

Merged
wsmoses merged 2 commits into
mainfrom
typetree-signed-offset-md
Aug 4, 2026
Merged

Serialize TypeTree offsets as signed constants#3070
wsmoses merged 2 commits into
mainfrom
typetree-signed-offset-md

Conversation

@wsmoses

@wsmoses wsmoses commented Aug 4, 2026

Copy link
Copy Markdown
Member

Fixes #3060Assertion \llvm::isUIntN(BitWidth, val) && "Value is not an N-bit unsigned value"'` on LLVM 23.

Root cause

TypeTree::toMD serializes offsets into !enzyme_type metadata:

llvm::ConstantInt::get(llvm::IntegerType::get(ctx, 32), pair.first)

pair.first is an int offset, and TypeTree uses -1 to mean "any offset". That converts to uint64_t 0xFFFFFFFFFFFFFFFF, which is not a 32-bit unsigned value.

LLVM changed the behavior under us:

ConstantInt::get(IntegerType*, uint64_t V, bool IsSigned)
LLVM ≤ 21 APInt(BitWidth, V, isSigned, /*implicitTrunc=*/true) — silently truncates
LLVM ≥ 23 APInt(Ty->getBitWidth(), V, IsSigned, ImplicitTrunc=false)asserts

Why it started firing now

This is a latent bug that only became reachable on this path in e4afe54 ("Speed up large constant ta"), which caches constant-global type analysis:

GV->setMetadata("enzyme_type", constTT.toMD(GV->getContext()));

Rust hits it on essentially every autodiff kernel, since its panic-location globals get a TypeTree containing -1 offsets (!{!"Unknown", i32 -1, ...}).

Backtrace from a local LLVM 23 build:

llvm::APInt::APInt(unsigned, unsigned long, bool, bool)
llvm::ConstantInt::get(llvm::IntegerType*, unsigned long, bool, bool)
TypeTree::toMD(llvm::LLVMContext&)
getConstantAnalysis(...)
TypeAnalyzer::getAnalysis / getCallInfo / visitIPOCall ...
EnzymeBase::HandleAutoDiff

Note this is not LLVM-23-only — LLVM 24 asserts identically. The opt21 link in the issue passes only because of the implicit truncation.

Fix

Pass IsSigned=true. This is bit-identical to the old truncating behavior (-10xFFFFFFFF) and matches the reader side, insertFromMD, which already uses getSExtValue().

Two other ConstantInt::get(Ty, -1) sites found by audit are fixed the same way (MustExitScalarEvolution.cpp, Utils.cpp:nextPowerOfTwo). Both would assert identically on LLVM ≥ 23 for sub-64-bit types; I could not construct an input that reaches either, so they carry no dedicated test.

Testing

New test test/Enzyme/ForwardMode/globalconsttypemd.ll is reduced from the issue reproducer and pins the emitted !enzyme_type offsets. Verified it fails (asserts) without the fix and passes with it, on LLVM 21, 23, and 24.

check-enzyme on LLVM 24, before vs. after:

Passed Failed isUIntN aborts
baseline 287 752 30
with fix 290 749 0

The 30 aborts were spread across existing tests (ForwardMode/globalfn.ll, ReverseMode/storeconstexpr.ll, the ProbProg and omp* tests, …). The remaining failures are pre-existing CHECK-line drift for LLVM 23/24 IR printing, unrelated to this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P65yn8LELKWU1dq6AQvA5f

wsmoses and others added 2 commits August 4, 2026 17:03
TypeTree offsets may be negative -- -1 denotes "any offset" -- but
TypeTree::toMD emitted them via ConstantInt::get without IsSigned, so -1
became uint64_t 0xFFFFFFFFFFFFFFFF for an i32.

LLVM <= 21 silently truncated this (ConstantInt::get passed
implicitTrunc=true). As of LLVM 23 the default is implicitTrunc=false,
so the same call now trips

  APInt.h: Assertion `llvm::isUIntN(BitWidth, val) &&
           "Value is not an N-bit unsigned value"' failed.

This became reachable on LLVM >= 23 in e4afe54, which caches constant
global type analysis via GV->setMetadata("enzyme_type", ...). Rust's
autodiff support hits it on essentially every kernel, since its panic
location globals get a TypeTree containing -1 offsets.

Sign-extension is bit-identical to the previous truncating behavior
(-1 -> 0xFFFFFFFF) and matches insertFromMD, which already reads the
offsets back with getSExtValue.

Also fix two other ConstantInt::get(Ty, -1) sites found by audit that
would assert the same way on LLVM >= 23 for sub-64-bit types.

Fixes #3060

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P65yn8LELKWU1dq6AQvA5f
The test uses opaque pointers, but %newLoadEnzyme appends
-opaque-pointers=0 on LLVM 16, so `ptr` failed to parse. Use the
%OPnewLoadEnzyme substitution that exists for exactly this case, rather
than an explicit -opaque-pointers flag (which LLVM >= 17 rejects).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P65yn8LELKWU1dq6AQvA5f
@wsmoses
wsmoses merged commit 50c11dc into main Aug 4, 2026
35 checks passed
@wsmoses
wsmoses deleted the typetree-signed-offset-md branch August 4, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LLVM 23 unsupported: Assertion `llvm::isUIntN(BitWidth, val) && "Value is not an N-bit unsigned value"'

1 participant