Skip to content

Automatically mark read-only lambda captures as constant - #464

Open
davidbeckingsale wants to merge 23 commits into
mainfrom
task/auto-lambda-const
Open

Automatically mark read-only lambda captures as constant#464
davidbeckingsale wants to merge 23 commits into
mainfrom
task/auto-lambda-const

Conversation

@davidbeckingsale

Copy link
Copy Markdown
Collaborator

No description provided.

davidbeckingsale and others added 19 commits April 1, 2026 18:48
This header provides infrastructure for auto-detecting read-only lambda
captures at JIT compilation time. It includes:

- CaptureInfo struct to track capture metadata (offset, slot, type)
- analyzeReadOnlyCaptures() to identify read-only scalar captures
- isSupportedScalarType() to filter supported types (i1, i8, i32, i64, float, double)
- pointerEscapes() for conservative escape analysis
- mergeCaptures() to combine auto-detected and explicit captures

This is the foundation for automatic specialization of read-only captures,
reducing the need for explicit jit_variable annotations.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…evice

This change integrates the auto-detection of read-only lambda captures
into the GPU JIT compilation path (JitEngineDevice::compileAndRun).

Key changes:
- Added AutoReadOnlyCaptures.h include to JitEngineDevice.h
- Added traceOutAuto overload for RuntimeConstant in AutoReadOnlyCaptures.h
  to enable tracing of auto-detected captures during extraction
- Modified getLambdaJitValues to accept KernelArgs parameter and perform
  auto-detection when PROTEUS_AUTO_READONLY_CAPTURES is enabled
- Added findLambdaArgIndex helper to determine lambda argument position
- Auto-detected captures are merged with explicit jit_variable() captures,
  with explicit captures taking precedence
- Auto-detected captures are traced with [LambdaSpec][Auto] prefix
- Updated compileAndRun to pass KernelArgs to getLambdaJitValues
- Auto-detected captures are included in hash computation for correct caching

The implementation follows the flow:
1. Analyze lambda function IR for read-only captures
2. Extract capture values from lambda closure in KernelArgs
3. Merge with explicit captures (explicit takes precedence)
4. Trace auto-detected captures
5. Include merged captures in specialization hash

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit implements automatic detection and extraction of read-only lambda
captures in the CPU JIT compilation path (JitEngineHost), completing the
integration of the auto-readonly capture feature for host execution.

Changes:
- Modified JitEngineHost.cpp to include AutoReadOnlyCaptures.h
- Extended getLambdaJitValues() to perform auto-detection when enabled:
  * Analyzes IR to identify read-only captures using analyzeReadOnlyCaptures()
  * Infers closure type from lambda function arguments
  * Extracts capture values from lambda closure memory
  * Merges auto-detected captures with explicit jit_variable() captures
  * Generates [LambdaSpec][Auto] trace output for auto-detected captures
- Updated specializeIR() signature to accept merged lambda capture values
- Auto-detected captures are included in hash computation for cache correctness
- Feature respects PROTEUS_AUTO_READONLY_CAPTURES configuration option

The implementation follows the same pattern as JitEngineDevice, with
adaptations for the host JIT execution model where the lambda closure is
passed directly as Args[0].

All lambda tests pass, including tests for explicit captures, auto-detected
captures, mixed captures, and written (non-readonly) captures.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit makes progress toward enabling auto-readonly lambda capture
detection for CPU/host JIT execution, but the feature remains incomplete
due to missing closure pointer plumbing in the ProteusPass.

Changes made:
- Modified LambdaRegistry::registerLambda() to always register lambda types
  in the map, even when there are no explicit jit_variable() calls. This
  allows matchJitVariableMap() to find lambdas that rely solely on auto-
  detection.

- Updated tests/cpu/lambda_auto_readonly.cpp to enable auto-detection
  (PROTEUS_AUTO_READONLY_CAPTURES=1) and add CHECK-DAG assertions for
  [LambdaSpec][Auto] trace output, matching the GPU test configuration.

Root cause of remaining issue:
The auto-detection logic in getLambdaJitValues() requires the lambda
closure pointer to extract capture values from memory. For host JIT, this
pointer should be passed as Args[0]. However, the ProteusPass currently
only populates Args when there are explicit jit_variable() calls. For
lambdas with pure auto-detection (no jit_variable calls), Args is NULL,
preventing auto-detection from running.

Next steps:
The ProteusPass needs to be modified to ALWAYS pass the lambda closure
pointer (the 'this' pointer of the lambda operator()) as Args[0], even
when there are no explicit jit_variable() calls. This is the "closure
pointer plumbing" referenced in proteus-0sf.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit completes the integration of auto-readonly lambda capture
detection for CPU/host JIT execution, fixing the missing trace lines issue
reported in proteus-oub.

Root Causes Fixed:
1. ProteusPass was not passing Args when there were no explicit jit_variable()
   calls, preventing auto-detection from accessing the lambda closure pointer
2. AutoReadOnlyCaptures analysis only handled typed struct GEPs, but host IR
   uses byte-offset GEPs after optimization
3. ClosureType inference failed for byte-offset GEPs, requiring fallback to
   direct byte-offset extraction

Changes:
- src/pass/ProteusPass.cpp: Modified emitJitEntryCall() to always create Args
  array for lambda functions (detected by ::operator() in demangled name), even
  when NumRuntimeConstants == 0. This ensures the lambda closure pointer is
  available for auto-detection.

- include/proteus/AutoReadOnlyCaptures.h: Extended analyzeReadOnlyCaptures() to
  handle both typed struct GEPs and byte-offset GEPs. Uses byte offset directly
  as slot index for untyped GEPs to avoid collisions.

- src/lib/JitEngineHost.cpp: Added fallback in getLambdaJitValues() to extract
  captures using byte offsets when ClosureType is unavailable. Fixed Args[0]
  dereference to get actual closure pointer (pointer-to-pointer due to ABI).

- tests/cpu/lambda_auto_readonly.cpp: Updated test to expect i8 instead of i1
  for bool captures, matching the actual IR representation on CPU.

All CPU lambda tests now pass with PROTEUS_AUTO_READONLY_CAPTURES=1:
- lambda_auto_readonly
- lambda_written_captures
- lambda_mixed_captures
- lambda_pointer_captures

Resolves: proteus-oub

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add documentation for the PROTEUS_AUTO_READONLY_CAPTURES environment
variable to the user configuration guide. This option enables automatic
detection of read-only lambda captures for JIT specialization, allowing
scalar captures (int, float, double, bool) that are read-only within
the lambda body to be automatically specialized without requiring
explicit jit_variable() annotation. Default value is 1 (enabled).

Resolves beads task proteus-uqm.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes four issues that caused test failures:

1. Lambda registration cache loss: registerLambda was unconditionally
   overwriting JitVariableMap on every call, clearing explicit captures.
   Now only updates map if new registration or has pending variables.

2. Floating-point formatting: traceOutAuto used %e (scientific notation)
   producing "3.140000e+00" instead of "3.14". Changed to %g for compact
   representation.

3. Boolean type representation: Test expected i1 but C++ ABI stores bool
   as 1-byte (i8). Updated test expectation to match actual IR.

4. Capture order dependency: Tests used CHECK which requires specific
   order. Changed to CHECK-DAG to allow captures in any order since
   struct layout determines ordering.

All 6 tests now pass: lambda_def, lambda_pointer_captures (CPU/GPU),
lambda_auto_readonly.HIP, lambda_def.HIP, and lambda_def.HIP.rdc.

Co-Authored-By: Claude (claude-sonnet-4.5) <noreply@anthropic.com>
Update the CPU lambda_written_captures test to match the GPU equivalent
by enabling PROTEUS_AUTO_READONLY_CAPTURES=1 and adding CHECK lines to
verify auto-detection behavior:
- A (i32 10) and C (i32 30) are auto-detected as read-only
- B (i32 20) is correctly excluded because it's written in the lambda

This ensures both CPU and GPU tests verify the same auto-detection
functionality.

Fixes: proteus-5of

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@johnbowen42 johnbowen42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should see if using getModRefInfo gets us good checks on if a "slot"`, or GEP into a closure pointer, is read/written. If it gives us reasonable results, I think we should use that method. This would greatly simplify the analysis here and simplify/eliminate the def-use analysis

return 0;
}

// CHECK-DAG: [LambdaSpec][Auto] Replacing slot {{[0-9]+}} with double 3.14

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see B is only read in the lambda--does that mean it should be constant in IR? If so, it would be nice if this message said that explicitly in the test

bool IsReadOnly = true;
};

enum class PointerUseEffect {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we potentially replace this with the existing ModRefResult enum?

return PointerUseEffect::Ignore;
}

if (isa<CallBase>(U))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add an easy check here for if the Ptr is passed as a const value to the CB

continue;
}

PointerUseEffect Effect = classifyPointerUse(U, V);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just call getModRefInfo here on the current Instruction

}

PointerUseEffect Effect = classifyPointerUse(U, V);
if (Effect == PointerUseEffect::Ignore ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here you would check the ModRefInfo enum struct


auto TopLevelSlot = getTopLevelSlotIndex(ClosureArg, V, GEP);
int32_t ByteOffset = *LocalOffset;
analyzePointerUsersForSlot(F, ClosureArg, GEP,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might even be able to simplify this DFS analysis by collecting all the "slots", or any GEP/load into the ClosureArg, and simply do

DenseSet<Slot> isModified;
for (Slot : Slots) {
  for (auto &BB : F) {
      for (auto &I : BB) {
        if (getModRefInfo(Slot, I) != ModRefInfo::Ref)
          continue;
        isModified.insert(Slot);
       }
    }
}
  

then we mark as const an slot that isn't contained within isModified. I might be missing something here but I think it might simplify this analysis and reuse existing LLVM pointer analysis tools, which would both be good. It's possible LLVM is too conservative with getModRefInfo for our purposes here, which would be interesting

Comment thread src/pass/ProteusPass.cpp

// Check if this is a lambda function (contains ::operator() in demangled
// name)
std::string DemangledName = llvm::demangle(StubFn->getName().str());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to change a little bit with the new lambda handling. Firstly, we have both LambdaFunctorWrapper::operator() methods, as well as underlying lambda::operator() methods. This check will pass for both, as well as any operator() named function, which we don't want. I would simply change this to check if the metadata contains the string proteus.wrapper_call, which indicates we are handling a registered functor

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.

2 participants