Repository: shader-slang/slang (2026-03-25T17:49:52Z)

Issue:
Regression: Crash (assert failure) in autodiff pass after PR #10391
# Issue Description
After https://github.com/shader-slang/slang/pull/10391 was merged, compiling a `slang.neural` shader targeting CUDA with autodiff (`bwd_diff`) crashes with an assertion failure in the autodiff pass. The crash occurs in `AutoDiffPass::processReferencedFunctions` where `addToWorkList` receives a null operand (`SLANG_ASSERT(inst)` fails in `slang-ir-inst-pass-base.h:20`).

PR #10391 changed `_validateCircularVarDefinition` to use `ConstantFoldingKind::SpecializationConstant` instead of `LinkTime`, and added a new lowering path for `FuncCallIntVal` in `lowerConstantDeclCommon`. This appears to affect IR generation for `static const` globals in ways that produce null operands in the IR, which the autodiff pass then trips over.

# Reproducer Code

Save the following as `test.slang` and compile with:
```
slangc test.slang -experimental-feature -target cuda
```

```slang
import slang.neural;

#ifndef TCNN_MLP_IN_DIM
#define TCNN_MLP_IN_DIM 2
#endif

#ifndef WT_WAVE_WARPS
#define WT_WAVE_WARPS 4
#endif

static const int IN_DIM = TCNN_MLP_IN_DIM;
static const int HIDDEN_DIM = 128;
static const int OUT_DIM = 3;
static const int SubgroupSize = 32;
static const int WARPS_PER_BLOCK = WT_WAVE_WARPS;
static const float LEAKY_ALPHA = 0.01f;

typealias Storage = TorchTensorViewAddress<half>;
typealias ShMemSize = SharedMemorySize<half, TargetEnum.CUDA, ExecutionMode.Training, SubgroupSize, WARPS_PER_BLOCK>;
typealias ShMemSizeLayer = ShMemSize.OfLayer4<IN_DIM, HIDDEN_DIM, HIDDEN_DIM, HIDDEN_DIM, OUT_DIM>;
typealias InVec = WaveTangledVector<half, ShMemSizeLayer, IN_DIM, SubgroupSize>;
typealias HidVec = WaveTangledVector<half, ShMemSizeLayer, HIDDEN_DIM, SubgroupSize>;
typealias OutVec = WaveTangledVector<half, ShMemSizeLayer, OUT_DIM, SubgroupSize>;
typealias Layer1 = FFLayer<half, InVec, HidVec, LinearLayout, LeakyReLU<half>, true>;
typealias Layer2 = FFLayer<half, HidVec, HidVec, LinearLayout, LeakyReLU<half>, true>;
typealias Layer3 = FFLayer<half, HidVec, HidVec, LinearLayout, LeakyReLU<half>, true>;
typealias Layer4 = FFLayer<half, HidVec, OutVec, LinearLayout, Sigmoid<half>, true>;

// Flat parameter layout (contiguous): [L1(W+B), L2(W+B), L3(W+B), L4(W+B)]
static const int TotalParamCount =
    Layer1.ParameterCount + Layer2.ParameterCount + Layer3.ParameterCount + Layer4.ParameterCount;

[Differentiable]
OutVec runWaveMLP(InVec input, Storage params)
{
    LeakyReLU<half> leaky = LeakyReLU<half>(half(LEAKY_ALPHA));
    Layer1 layer1 = Layer1(leaky);
    Layer2 layer2 = Layer2(leaky);
    Layer3 layer3 = Layer3(leaky);
    Layer4 layer4 = Layer4();

    int offset = 0;
    HidVec h1 = layer1.eval<Storage>(input, params.getOffset(offset)); offset += Layer1.ParameterCount;
    HidVec h2 = layer2.eval<Storage>(h1,    params.getOffset(offset)); offset += Layer2.ParameterCount;
    HidVec h3 = layer3.eval<Storage>(h2,    params.getOffset(offset)); offset += Layer3.ParameterCount;
    return     layer4.eval<Storage>(h3,    params.getOffset(offset));
}

[AutoPyBindCUDA]
[CUDAKernel]
void backward(
    DiffTensorView input,
    TensorView<half> params,
    TensorView<half> paramsGrad,
    DiffTensorView output)
{
    uint idx = cudaBlockIdx().x * cudaBlockDim().x + cudaThreadIdx().x;
    bool isActive = idx < input.size(0);

    InVec x = InVec(half(0));
    if (isActive)
    {
        for (int i = 0; i < IN_DIM; ++i) x[i] = half(input.primal[idx, i]);
    }

    OutVec dL_dOutput = OutVec(half(0));
    if (isActive)
    {
        for (int i = 0; i < OUT_DIM; ++i) dL_dOutput[i] = half(output.diff.diff[idx, i]);
    }

    InVec dx = InVec(half(0));
    var dpInput = diffPair(x, dx);
    bwd_diff(runWaveMLP)(dpInput,
        DifferentialPtrPair<Storage>(Storage(params), Storage(paramsGrad)),
        dL_dOutput);

    if (isActive)
    {
        for (int i = 0; i < IN_DIM; ++i) input.diff.diff[idx, i] = float(dpInput.d[i]);
    }
}
```

# Expected Behavior
The shader should compile successfully to CUDA output, as it did before PR #10391.

# Actual Behavior
Compilation crashes with a segfault (Release) or assertion failure (Debug):

```
error[E99999]: Slang compilation aborted due to an exception of N5Slang13InternalErrorE assert failure: inst
```

Stack trace from debug build:
```
#0  __cxa_throw ()
#1  Slang::handleSignal (type=AssertFailure, message="inst") at slang-signal.cpp:117
#2  Slang::handleAssert (message="inst") at slang-signal.cpp:90
#3  Slang::InstPassBase::addToWorkList (inst=0x0) at slang-ir-inst-pass-base.h:20
#4  Slang::AutoDiffPass::processReferencedFunctions::{lambda}::operator() at slang-ir-autodiff.cpp:3558
#5  Slang::AutoDiffPass::processAllReachableInsts<...> at slang-ir-autodiff.cpp:3481
#6  Slang::AutoDiffPass::processReferencedFunctions at slang-ir-autodiff.cpp:3516
#7  Slang::AutoDiffPass::processModule at slang-ir-autodiff.cpp:2701
#8  Slang::processAutodiffCalls at slang-ir-autodiff.cpp:3830
```

# Environment
 - Slang Version: 2026.3.1
 - OS: Linux (x86_64)

# Additional context
The reproducer does not use `[vk::specialization_constant]` at all — the `static const` globals are initialized from plain integer literals and macro expansions. The crash appears to be a side effect of the `ConstantFoldingKind::SpecializationConstant` change in `_validateCircularVarDefinition` affecting the constant folding and lowering path for non-spec-constant globals, producing IR with null operands that the autodiff pass does not expect.



Discussion:
@aidanfnv @saipraveenb25 It sounds like we don't have a robust fix for this yet. Can we revert the PR that caused this regression, at least until we have a fix and can then re-apply? This regression is currently blocking #10609 from @kaizhangNV.



Patch:
diff --git a/source/slang/slang-lower-to-ir.cpp b/source/slang/slang-lower-to-ir.cpp
index b3abe91e485..cd1f732ebcb 100644
--- a/source/slang/slang-lower-to-ir.cpp
+++ b/source/slang/slang-lower-to-ir.cpp
@@ -9944,17 +9944,7 @@ struct DeclLoweringVisitor : DeclVisitor<DeclLoweringVisitor, LoweredValInfo>
             // with the value `5`, then we might have only a single
             // instruction to represent `5`.
             //
-            // FuncCallIntVal means the initializer was symbolically folded but
-            // not fully evaluated (operands are spec constants or generic value
-            // params). Lower via visitFuncCallIntVal which emits constexpr IR
-            // ops and propagates @SpecConst rate when any operand carries it.
-            // The raw initExpr path would emit plain ops without that rate.
-            //
-            IRInst* irInitVal;
-            if (auto funcCallIntVal = as<FuncCallIntVal>(decl->val))
-                irInitVal = lowerSimpleVal(subContext, funcCallIntVal);
-            else
-                irInitVal = getSimpleVal(subContext, lowerRValueExpr(subContext, initExpr));
+            auto irInitVal = getSimpleVal(subContext, lowerRValueExpr(subContext, initExpr));
 
             // We construct a distinct IR instruction to represent the
             // constant itself, with the value as an operand.


Test patch:
diff --git a/tests/bugs/neural-autodiff-constexpr-crash.slang b/tests/bugs/neural-autodiff-constexpr-crash.slang
new file mode 100644
index 00000000000..f2da333cef3
--- /dev/null
+++ b/tests/bugs/neural-autodiff-constexpr-crash.slang
@@ -0,0 +1,53 @@
+// Regression test for https://github.com/shader-slang/slang/issues/10605
+//
+// After PR #10391 changed constant folding to use SpecializationConstant
+// folding, `static const` members in generic structs computed from generic
+// params (e.g. division, ternary) get lowered as constexpr IR ops inside
+// the generic. When slang.neural types with such members (like
+// WaveTangledVector.Uint4AlignedInputSize) are used in differentiable
+// functions, the autodiff pass encounters an orphaned constexpr op (null
+// parent, null operands) still referenced by live IR, causing a segfault
+// in AutoDiffPass::processReferencedFunctions.
+//
+//TEST:SIMPLE(filecheck=CHECK): -target cuda -experimental-feature
+
+// CHECK: __global__ void backward
+// CHECK: s_bwd_run
+
+import slang.neural;
+
+static const int SubgroupSize = 32;
+static const int WARPS = 4;
+
+typealias ShMemSize = SharedMemorySize<half, TargetEnum.CUDA, ExecutionMode.Training, SubgroupSize, WARPS>;
+typealias ShMemSizeLayer = ShMemSize.OfLayer4<2, 128, 128, 128, 3>;
+typealias InVec = WaveTangledVector<half, ShMemSizeLayer, 2, SubgroupSize>;
+typealias HidVec = WaveTangledVector<half, ShMemSizeLayer, 128, SubgroupSize>;
+typealias OutVec = WaveTangledVector<half, ShMemSizeLayer, 3, SubgroupSize>;
+typealias L1 = FFLayer<half, InVec, HidVec, LinearLayout, LeakyReLU<half>, true>;
+typealias L2 = FFLayer<half, HidVec, OutVec, LinearLayout, Sigmoid<half>, true>;
+typealias Storage = TorchTensorViewAddress<half>;
+
+[Differentiable]
+OutVec run(InVec input, Storage params)
+{
+    LeakyReLU<half> leaky = LeakyReLU<half>(half(0.01));
+    L1 l1 = L1(leaky);
+    L2 l2 = L2();
+
+    int offset = 0;
+    HidVec h1 = l1.eval<Storage>(input, params.getOffset(offset)); offset += L1.ParameterCount;
+    return l2.eval<Storage>(h1, params.getOffset(offset));
+}
+
+[CUDAKernel]
+void backward(TensorView<half> params, TensorView<half> paramsGrad)
+{
+    InVec x = InVec(half(0));
+    OutVec dOut = OutVec(half(0));
+    InVec dx = InVec(half(0));
+    var dpInput = diffPair(x, dx);
+    bwd_diff(run)(dpInput,
+        DifferentialPtrPair<Storage>(Storage(params), Storage(paramsGrad)),
+        dOut);
+}
diff --git a/tests/spirv/spec-constant-select-global-const.slang b/tests/spirv/spec-constant-select-global-const.slang
index d72191c1816..080dda80d94 100644
--- a/tests/spirv/spec-constant-select-global-const.slang
+++ b/tests/spirv/spec-constant-select-global-const.slang
@@ -1,5 +1,5 @@
-//TEST:SIMPLE(filecheck=CHECK): -target spirv-asm -entry computeMain -stage compute
-//TEST(compute, vulkan):COMPARE_COMPUTE(filecheck-buffer=BUF):-vk -output-using-type -emit-spirv-directly
+//DISABLE_TEST:SIMPLE(filecheck=CHECK): -target spirv-asm -entry computeMain -stage compute
+//DISABLE_TEST(compute, vulkan):COMPARE_COMPUTE(filecheck-buffer=BUF):-vk -output-using-type -emit-spirv-directly
 
 //TEST_INPUT:ubuffer(data=[0 0 0 0], stride=4):out,name=outputBuffer
 RWStructuredBuffer<uint> outputBuffer;