Julia 1.12: Errors With `sum` And `reduce` On Float32
Introduction
If you're a Julia user who has recently upgraded to version 1.12, you might have encountered some unexpected behavior with the sum and reduce functions when working with Float32 data types. This article delves into a specific issue reported by a user, outlining the problem, providing context, and exploring potential causes and solutions. We'll break down the error messages, examine the code snippets, and discuss the implications for numerical computations in Julia. This comprehensive guide aims to help you understand and address these errors effectively, ensuring a smoother experience with Julia 1.12.
The Issue: Errors with sum and reduce in Julia 1.12
In Julia 1.12, a user reported that the sum function throws an error when used with Float32 arrays, while it works perfectly fine with Float64 arrays. Additionally, the reduce function exhibits similar issues, working with min but failing with max. This behavior is quite peculiar and can be disruptive for workflows that heavily rely on these functions, especially in numerical and scientific computing. To effectively tackle this, it’s crucial to dive into the specifics, examine the error messages, and understand the context in which these errors arise.
Error Encountered with sum
The user, who was working with the Reactant and Lux packages, noticed that the sum function produced an error specifically when applied to Float32 arrays. Here’s the code snippet that triggers the error:
julia> using Reactant, Lux
julia> device = reactant_device();
julia> x = rand(Float32, 10) |> device
10-element ConcreteIFRTArray{Float32,1}:
0.14851218
0.24151039
0.71405196
0.6278459
0.11392623
0.6585004
0.4605382
0.97142327
0.8887215
0.3047663
julia> @jit sum(Float64.(x))
ConcreteIFRTNumber{Float64}(5.129796326160431)
julia> @jit sum(x)
ERROR: Internal Error: Opaque closure with no source at all
Stacktrace:
[1] call_with_reactant
@ ./none:-1 [inlined]
[2] call_with_reactant(::typeof(Base.add_sum), ::Reactant.TracedRNumber{Float32}, ::Reactant.TracedRNumber{Float32})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/utils.jl:0
[3] make_mlir_fn(f::typeof(Base.add_sum), args::Tuple{…}, kwargs::Tuple{}, name::String, concretein::Bool; toscalar::Bool, return_dialect::Symbol, args_in_result::Symbol, construct_function_without_args::Bool, do_transpose::Bool, input_shardings::Nothing, output_shardings::Nothing, runtime::Nothing, verify_arg_names::Nothing, argprefix::Symbol, resprefix::Symbol, resargprefix::Symbol, num_replicas::Int64, optimize_then_pad::Bool)
@ Reactant.TracedUtils ~/.julia/packages/Reactant/zlIsO/src/TracedUtils.jl:345
[4] _construct_reduce_function(f::typeof(Base.add_sum), Ts::Type)
@ Reactant.Ops ~/.julia/packages/Reactant/zlIsO/src/Ops.jl:2920
[5] reduce(xs::Vector{…}, init_values::Vector{…}, dimensions::Vector{…}, fn::typeof(Base.add_sum); location::Reactant.MLIR.IR.Location)
@ Reactant.Ops ~/.julia/packages/Reactant/zlIsO/src/Ops.jl:3012
[6] reduce(x::Reactant.TracedRArray{…}, init_values::Reactant.TracedRNumber{…}, dimensions::Vector{…}, fn::typeof(Base.add_sum); location::Reactant.MLIR.IR.Location)
@ Reactant.Ops ~/.julia/packages/Reactant/zlIsO/src/Ops.jl:2998
[7] overloaded_mapreduce(f::Any, op::Any, A::Reactant.TracedRArray{Float32, 1}; dims::Function, init::Base._InitialValue)
@ Reactant.TracedRArrayOverrides ~/.julia/packages/Reactant/zlIsO/src/TracedRArray.jl:376
[8] overloaded_mapreduce
@ ~/.julia/packages/Reactant/zlIsO/src/TracedRArray.jl:351 [inlined]
[9] mapreduce(f::Function, op::Function, A::Reactant.TracedRArray{Float32, 1}; kwargs::@Kwargs{})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/Overlay.jl:181
[10] mapreduce(f::Function, op::Function, A::Reactant.TracedRArray{Float32, 1})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/Overlay.jl:174
[11] _sum
@ ./reducedim.jl:984 [inlined]
[12] _sum
@ ./reducedim.jl:983 [inlined]
[13] sum
@ ./reducedim.jl:979 [inlined]
[14] (::Nothing)(none::typeof(sum), none::Reactant.TracedRArray{Float32, 1})
@ Reactant ./<missing>:0
[15] call_with_reactant
@ ./none:-1 [inlined]
[16] call_with_reactant(::typeof(sum), ::Reactant.TracedRArray{Float32, 1})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/utils.jl:0
[17] make_mlir_fn(f::typeof(sum), args::Tuple{…}, kwargs::@NamedTuple{}, name::String, concretein::Bool; toscalar::Bool, return_dialect::Symbol, args_in_result::Symbol, construct_function_without_args::Bool, do_transpose::Bool, input_shardings::Nothing, output_shardings::Nothing, runtime::Val{…}, verify_arg_names::Nothing, argprefix::Symbol, resprefix::Symbol, resargprefix::Symbol, num_replicas::Int64, optimize_then_pad::Bool)
@ Reactant.TracedUtils ~/.julia/packages/Reactant/zlIsO/src/TracedUtils.jl:345
[18] make_mlir_fn
@ ~/.julia/packages/Reactant/zlIsO/src/TracedUtils.jl:275 [inlined]
[19] compile_mlir!(mod::Reactant.MLIR.IR.Module, f::typeof(sum), args::Tuple{…}, compile_options::CompileOptions, callcache::Dict{…}, sdycache::Dict{…}, sdygroupidcache::Tuple{…}; fn_kwargs::@NamedTuple{}, backend::String, runtime::Val{…}, legalize_stablehlo_to_mhlo::Bool, client::Reactant.XLA.IFRT.Client, kwargs::@Kwargs{…})
@ Reactant.Compiler ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:1614
[20] compile_xla(f::Function, args::Tuple{…}; before_xla_optimizations::Bool, client::Nothing, serializable::Bool, kwargs::@Kwargs{…})
@ Reactant.Compiler ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:3524
[21] compile_xla
@ ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:3496 [inlined]
[22] compile(f::Function, args::Tuple{…}; kwargs::@Kwargs{…})
@ Reactant.Compiler ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:3600
[23] top-level scope
@ ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:2669
Some type information was truncated. Use `show(err)` to see complete types.
Interestingly, the sum function works correctly when the Float32 array is first converted to Float64. This suggests the issue is specific to how sum is implemented or interacts with Float32 within the Reactant context. The error message "Internal Error: Opaque closure with no source at all" indicates a deeper problem, possibly related to the compilation or tracing mechanisms used by Reactant.
Error Encountered with reduce
The reduce function showed a similar discrepancy. While it worked as expected with min, it failed with max when applied to Float32 arrays. Here’s the relevant code:
julia> @jit reduce(min, x)
ConcreteIFRTNumber{Float32}(0.11392623f0)
julia> @jit reduce(max, x)
ERROR: Internal Error: Opaque closure with no source at all
Stacktrace:
[1] call_with_reactant
@ ./none:-1 [inlined]
[2] call_with_reactant(::typeof(max), ::Reactant.TracedRNumber{Float32}, ::Reactant.TracedRNumber{Float32})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/utils.jl:0
[3] make_mlir_fn(f::typeof(max), args::Tuple{…}, kwargs::Tuple{}, name::String, concretein::Bool; toscalar::Bool, return_dialect::Symbol, args_in_result::Symbol, construct_function_without_args::Bool, do_transpose::Bool, input_shardings::Nothing, output_shardings::Nothing, runtime::Nothing, verify_arg_names::Nothing, argprefix::Symbol, resprefix::Symbol, resargprefix::Symbol, num_replicas::Int64, optimize_then_pad::Bool)
@ Reactant.TracedUtils ~/.julia/packages/Reactant/zlIsO/src/TracedUtils.jl:345
[4] _construct_reduce_function(f::typeof(max), Ts::Type)
@ Reactant.Ops ~/.julia/packages/Reactant/zlIsO/src/Ops.jl:2920
[5] reduce(xs::Vector{…}, init_values::Vector{…}, dimensions::Vector{…}, fn::typeof(max); location::Reactant.MLIR.IR.Location)
@ Reactant.Ops ~/.julia/packages/Reactant/zlIsO/src/Ops.jl:3012
[6] reduce(x::Reactant.TracedRArray{…}, init_values::Reactant.TracedRNumber{…}, dimensions::Vector{…}, fn::typeof(max); location::Reactant.MLIR.IR.Location)
@ Reactant.Ops ~/.julia/packages/Reactant/zlIsO/src/Ops.jl:2998
[7] overloaded_mapreduce(f::Any, op::Any, A::Reactant.TracedRArray{Float32, 1}; dims::Function, init::Base._InitialValue)
@ Reactant.TracedRArrayOverrides ~/.julia/packages/Reactant/zlIsO/src/TracedRArray.jl:376
[8] overloaded_mapreduce
@ ~/.julia/packages/Reactant/zlIsO/src/TracedRArray.jl:351 [inlined]
[9] mapreduce(f::Function, op::Function, A::Reactant.TracedRArray{Float32, 1}; kwargs::@Kwargs{})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/Overlay.jl:181
[10] mapreduce(f::Function, op::Function, A::Reactant.TracedRArray{Float32, 1})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/Overlay.jl:174
[11] reduce
@ ./reducedim.jl:375 [inlined]
[12] (::Nothing)(none::typeof(reduce), none::typeof(max), none::Reactant.TracedRArray{Float32, 1})
@ Reactant ./<missing>:0
[13] call_with_reactant
@ ./none:-1 [inlined]
[14] call_with_reactant(::typeof(reduce), ::typeof(max), ::Reactant.TracedRArray{Float32, 1})
@ Reactant ~/.julia/packages/Reactant/zlIsO/src/utils.jl:0
[15] make_mlir_fn(f::typeof(reduce), args::Tuple{…}, kwargs::@NamedTuple{}, name::String, concretein::Bool; toscalar::Bool, return_dialect::Symbol, args_in_result::Symbol, construct_function_without_args::Bool, do_transpose::Bool, input_shardings::Nothing, output_shardings::Nothing, runtime::Val{…}, verify_arg_names::Nothing, argprefix::Symbol, resprefix::Symbol, resargprefix::Symbol, num_replicas::Int64, optimize_then_pad::Bool)
@ Reactant.TracedUtils ~/.julia/packages/Reactant/zlIsO/src/TracedUtils.jl:345
[16] make_mlir_fn
@ ~/.julia/packages/Reactant/zlIsO/src/TracedUtils.jl:275 [inlined]
[17] compile_mlir!(mod::Reactant.MLIR.IR.Module, f::typeof(reduce), args::Tuple{…}, compile_options::CompileOptions, callcache::Dict{…}, sdycache::Dict{…}, sdygroupidcache::Tuple{…}; fn_kwargs::@NamedTuple{}, backend::String, runtime::Val{…}, legalize_stablehlo_to_mhlo::Bool, client::Reactant.XLA.IFRT.Client, kwargs::@Kwargs{…})
@ Reactant.Compiler ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:1614
[18] compile_xla(f::Function, args::Tuple{…}; before_xla_optimizations::Bool, client::Nothing, serializable::Bool, kwargs::@Kwargs{…})
@ Reactant.Compiler ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:3524
[19] compile_xla
@ ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:3496 [inlined]
[20] compile(f::Function, args::Tuple{…}; kwargs::@Kwargs{…})
@ Reactant.Compiler ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:3600
[21] top-level scope
@ ~/.julia/packages/Reactant/zlIsO/src/Compiler.jl:2669
Some type information was truncated. Use `show(err)` to see complete types.
This inconsistency suggests that the issue might be related to specific implementations or optimizations within Reactant for different reduction operations (min vs. max). The stack trace again points to problems during the compilation phase, particularly within the Reactant.Compiler module.
Julia Version and Package Status
The user provided their Julia version information, which is crucial for diagnosing the issue:
Julia Version 1.12.2
Commit ca9b6662be4 (2025-11-20 16:25 UTC)
Build Info:
Official https://julialang.org release
Platform Info:
OS: Linux (x86_64-linux-gnu)
CPU: 112 Ă— AMD EPYC 7453 28-Core Processor
WORD_SIZE: 64
LLVM: libLLVM-18.1.7 (ORCJIT, znver3)
GC: Built with stock GC
Threads: 112 default, 1 interactive, 112 GC (on 112 virtual cores)
Additionally, the status of the relevant packages was provided:
(Test) pkg> status
Status `~/Test/Project.toml`
[052768ef] CUDA v5.9.5
[b2108857] Lux v1.27.0
[21216c6a] Preferences v1.5.0
[3c362404] Reactant v0.2.180
This information helps in understanding the software environment in which the errors occurred. Knowing the versions of Julia, Reactant, Lux, and other dependencies can narrow down potential compatibility issues or bugs specific to certain versions.
Diving Deeper: Potential Causes and Solutions
Now that we've laid out the specific errors and the environment, let’s delve into the potential causes and what steps can be taken to resolve them. The error message “Internal Error: Opaque closure with no source at all” strongly suggests that the issue lies within the compilation process of Reactant. This could be due to how Reactant handles Float32 types, or it could be a bug in the tracing or optimization stages.
1. Reactant's Handling of Float32
Reactant is a package that focuses on differentiable programming, and it employs techniques like tracing and just-in-time (JIT) compilation to optimize performance. It’s possible that there are specific code paths or optimizations that are not fully compatible with Float32 data types. This could manifest as an inability to trace or compile certain operations, leading to the