We at AMD propose a target-independent LLVM IR intrinsic for querying whether debugging is currently enabled for a target-defined execution context. An initial AMDGPU lowering is proposed for selected GFX-family targets.
Disclaimer: AI was used to improve upon the initial draft of this RFC text, and was subsequently reviewed and edited.
Proposal
Introduce the following intrinsic:
declare noundef i1 @llvm.is.debugging.enabled() nomerge memory(inaccessiblemem: readwrite)
The intrinsic returns whether debugging is enabled for a target-defined execution context associated with the current invocation.
Each dynamically executed invocation is a distinct observation of the
debugging state. LLVM must not assume that separate calls return the same value.
Calls cannot be removed when unused, commoned, merged, or reordered relative to
another call.
The inaccessible-memory effect models debugger or runtime state that is not
otherwise represented in the program. nomerge preserves distinct call sites.
On targets that do not support querying debugging state, the intrinsic returns
false and performs no observation.
Motivation
This work is directly motivated by Microsoft’s official HLSL/DX12 Debugging
Intrinsics proposal.
That proposal introduces DebugBreak() and dx::IsDebuggingEnabled() so shader
authors can enable expensive validation only during debugging and trigger a
breakpoint when a failure is detected.
LLVM already provides llvm.debugtrap for requesting debugger attention, but
there is no target-independent way to query whether debugging is enabled.
Without such an intrinsic, frontends must use target-specific operations or
opaque calls, preventing consistent optimization and backend lowering.
The proposed intrinsic supplies that missing query while allowing each backend
to define the relevant execution context and debugging state.
Example usage
At the HLSL level:
if (dx::IsDebuggingEnabled()) {
DebugBreak();
}
This can be lowered to roughly the following LLVM IR:
entry:
%enabled = call i1 @llvm.is.debugging.enabled()
br i1 %enabled, label %debug, label %normal
debug:
call void @llvm.debugtrap()
br label %normal
normal:
...
A frontend may independently mark the debugging path as unlikely, e.g.,
using llvm.expect, which lets the optimizer lay that path
out as cold code.
AMDGPU implementation and lowering example
The initial AMDGPU implementation supports GFX11.5 and GFX12/13-family targets.
Other subtargets lower the intrinsic to false.
The target-defined execution context is the current wave. The result is uniform
across the wave’s active lanes, including when the intrinsic is executed in
divergent control flow. A wave is in debugging mode when either COND_DBG_SYS
(system or external-debugger enablement) or COND_DBG_USER (per-dispatch
enablement, configured through CDBG_USER in the kernel descriptor) is set.
Both SelectionDAG and GlobalISel lower the intrinsic in one of two ways:
- When the result reaches a conditional branch in the same basic block and
fusion is safe, the query and branch are combined into a single
s_cbranch_cdbgsys_or_user. Thus, no boolean value is materialized. Canonical
negated conditions still fuse. - For other uses, the result is materialized by reading the
debugging-enabled bits from the wave status register and comparing them
against zero. The register and bit positions may vary by generation.
Fusion moves the observation from the intrinsic to the branch, so it is not
performed across an intervening memory operation, call, barrier, fence, or
other observable side effect. Pure arithmetic and other non-observable
operations do not prevent fusion.
For example:
%enabled = call i1 @llvm.is.debugging.enabled()
%expected = call i1 @llvm.expect.i1(i1 %enabled, i1 false) ; optional
br i1 %expected, label %debug, label %normal
lowers to a single branch instruction:
s_cbranch_cdbgsys_or_user .Ldebug
.Lnormal:
... ; hot path
.Ldebug:
... ; cold debugging code
Other backends
Backends are not required to implement target-specific lowering. By default,
LLVM replaces llvm.is.debugging.enabled with false before instruction
selection.
A backend that can observe native debugger state may opt in through
TargetMachine::canLowerIsDebuggingEnabled(). It must then:
- Define the target-specific execution context and debugging-enabled state.
- Provide lowering for supported subtargets.
- Lower the intrinsic to
falseon any unsupported subtargets.
This keeps the intrinsic usable in target-independent IR without imposing
implementation work on existing backends.
Feedback requested
We welcome feedback on the proposed intrinsic, its usefulness to the broader
community, and any concerns not adequately addressed here.
For implementation context and technical details, see the following list of four PR’s that build upon each other:
- [AMDGPU][GlobalISel][SelectionDAG] Refactor control-flow intrinsic branch matching
- [IR][CodeGen] Add llvm.is.debugging.enabled intrinsic
- [AMDGPU][GFX12/GFX13] Add cdbg branch support and lower llvm.is.debugging.enabled
- [AMDGPU] Add AMDPAL support for llvm.debugtrap
Thanks in advance!