From 01f293ed9d8c228b0b742aed2527ca063e10ca9e Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Sun, 6 Sep 2026 19:40:20 -0400 Subject: [PATCH 1/5] Add familiar bounded reductions --- Project.toml | 4 +- RELEASE_NOTES.md | 9 ++ docs/src/api/localmath.md | 18 ++++ src/LocalMath.jl | 4 +- src/authoring/syntax.jl | 10 +- src/bounded_reductions.jl | 154 ++++++++++++++++++++++++++++++ test/metal/localmath_authoring.jl | 35 +++++++ test/metal/runtests.jl | 1 + test/runtests.jl | 1 + test/test_bounded_reductions.jl | 121 +++++++++++++++++++++++ test/test_public_api.jl | 2 +- 11 files changed, 353 insertions(+), 6 deletions(-) create mode 100644 src/bounded_reductions.jl create mode 100644 test/test_bounded_reductions.jl diff --git a/Project.toml b/Project.toml index c8aaa45..aa4bbd2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LocalMath" uuid = "82808884-8acc-4a5c-a056-be440a0fbea1" -version = "0.2.0-rc1" +version = "0.2.0-rc2" authors = ["Praneeth Merugu "] [deps] @@ -8,6 +8,7 @@ Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" Atomix = "a9b6321e-bd34-4604-b9c9-b65b8de01458" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" @@ -16,6 +17,7 @@ Adapt = "4.7" Atomix = "1.1" KernelAbstractions = "0.9.42" StaticArrays = "1" +Statistics = "1.11" StructArrays = "0.7" UUIDs = "1.11" julia = "1.12" diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4b26827..7fab50a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,12 @@ +# LocalMath 0.2.0-rc2 + +- Replaces callable-first bounded-fold authoring with `LocalMath.fold(values; + ...)`, while retaining the checked `BoundedFold` compiler law. +- Adds familiar `sum`, `minimum`, `maximum`, `Statistics.mean`, and qualified + `LocalMath.geometric_mean` methods for bounded relation and Collection views. +- Gives all bounded reductions explicit result-type, empty-input, absence, and + canonical-order semantics on the existing transaction-aware executor. + # LocalMath 0.2.0-rc1 - First independently versioned release candidate of the typed local scientific-computation language. diff --git a/docs/src/api/localmath.md b/docs/src/api/localmath.md index f37a60b..97bcbe0 100644 --- a/docs/src/api/localmath.md +++ b/docs/src/api/localmath.md @@ -121,6 +121,24 @@ closes the map, accumulator, and result types before planning. second executor; the canonical implementation remains valid. Bounded scalar folding is distinct from the `OrderedFold` publication law for evolving state. +Common reductions use ordinary Julia vocabulary on those same bounded views: + +```julia +total = sum(values) +smallest = minimum(values) +largest = maximum(values) +average = Statistics.mean(values) +geometric = LocalMath.geometric_mean(values) +``` + +Canonical relation or Collection order is retained and repeated endpoints +participate repeatedly. Absent lanes do not participate. `sum` returns the +typed additive identity on empty input; `minimum` and `maximum` reject an empty +transaction; `Statistics.mean` returns the ordinary correctly typed `NaN` for +empty input. Present `NaN`, infinity, and signed zero follow Julia's ordinary +numeric operations. `LocalMath.geometric_mean` accepts concrete floating-point +values and rejects empty input or a nonpositive present value. + ## Inspection `LocalMath.inspect` is qualified because it is tooling rather than ordinary diff --git a/src/LocalMath.jl b/src/LocalMath.jl index 6102ce5..000121c 100644 --- a/src/LocalMath.jl +++ b/src/LocalMath.jl @@ -39,7 +39,7 @@ public ArgMin, ArgMax, CanonicalSourceLaneTie, TieMin, TieMax public RejectOverflow, EmptyCollection public FoldComponent, InitializedState, BoundedWrites, FoldStep public initialized_state -public BoundedFold, fold, Where +public BoundedFold, fold, geometric_mean, Where public RejectInvalid, SkipInvalid, FillInvalid, RejectEmpty public RelaxedAssociative public BoundedFoldOutcome, evaluate_bounded @@ -51,6 +51,7 @@ import Adapt import Atomix import KernelAbstractions import StaticArrays +import Statistics import StructArrays import UUIDs using KernelAbstractions: @index, @kernel, @localmem, @synchronize @@ -77,6 +78,7 @@ include("stage_planning.jl") include("execution/relation_preparation.jl") include("execution/stage_preparation.jl") include("execution/bounded_fold_support.jl") +include("bounded_reductions.jl") include("execution/workspace_support.jl") include("execution/candidate_grouping.jl") include("execution/candidate_stage.jl") diff --git a/src/authoring/syntax.jl b/src/authoring/syntax.jl index dd28d4d..3af5cf1 100644 --- a/src/authoring/syntax.jl +++ b/src/authoring/syntax.jl @@ -1152,14 +1152,18 @@ function _lm_emit_stage( end function _lm_contains_fold_call(value) - value isa GlobalRef && - return value.mod === LocalMath && value.name === :fold + fold_names = (:fold, :minimum, :maximum, :mean, :geometric_mean) + value isa GlobalRef && return value.name in fold_names value isa QuoteNode && return false value isa Expr || return false + if value.head === :call && !isempty(value.args) + callee = first(value.args) + callee isa Symbol && callee in fold_names && return true + end if value.head === :. && length(value.args) == 2 name = value.args[2] name = name isa QuoteNode ? name.value : name - name === :fold && return true + name in fold_names && return true end return any(_lm_contains_fold_call, value.args) end diff --git a/src/bounded_reductions.jl b/src/bounded_reductions.jl new file mode 100644 index 0000000..34a4fbd --- /dev/null +++ b/src/bounded_reductions.jl @@ -0,0 +1,154 @@ +const _BoundedPrimitiveNumber = Union{ + Bool, + Int8, Int16, Int32, Int64, + UInt8, UInt16, UInt32, UInt64, + Float16, Float32, Float64, +} + +struct _BoundedConvert{T} end +@inline (::_BoundedConvert{T})(value) where {T} = convert(T, value) + +struct _BoundedResult end +@inline (::_BoundedResult)(accumulator, ::Int32) = accumulator + +struct _BoundedMeanResult{T} end +@inline (::_BoundedMeanResult{T})(accumulator, count::Int32) where {T} = + accumulator / T(count) + +struct _BoundedGeometricMap end +@inline (::_BoundedGeometricMap)(value) = log(value) + +struct _BoundedGeometricResult{T} end +@inline (::_BoundedGeometricResult{T})(accumulator, count::Int32) where {T} = + exp(accumulator / T(count)) + +struct _BoundedPositive end +@inline (::_BoundedPositive)(value) = value > zero(value) + +_bounded_sum_type(::Type{Bool}) = Int +_bounded_sum_type(::Type{T}) where {T<:Signed} = promote_type(Int, T) +_bounded_sum_type(::Type{T}) where {T<:Unsigned} = promote_type(UInt, T) +_bounded_sum_type(::Type{T}) where {T<:AbstractFloat} = T + +_bounded_mean_type(::Type{T}) where {T<:Integer} = Float64 +_bounded_mean_type(::Type{T}) where {T<:AbstractFloat} = T + +function _bounded_reduction_input_type(::Type{V}) where {V} + sample_type = Core.Compiler.return_type(getindex, Tuple{V,Int}) + if sample_type isa DataType && sample_type <: _StageSample + return Base.unwrap_unionall(sample_type).parameters[1] + end + return sample_type +end + +function _unsupported_bounded_reduction(name::Symbol, ::Type{T}) where {T} + throw(LocalMathValidationError( + "$(name) does not support this bounded value type"; + stage = :construct, + contract = :bounded_reduction_element_type, + expected = _BoundedPrimitiveNumber, + actual = T, + )) +end + +_inline_bounded_reduction(expression) = + Expr(:block, Expr(:meta, :inline), expression) + +@inline function _bounded_sum_typed(values, ::Type{T}, ::Type{R}) where {T,R} + operation = _bounded_fold( + _BoundedConvert{R}(), +, zero(R), _BoundedResult(), + _AllBoundedValues(), RejectInvalid(), FillEmpty(zero(R)), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_minimum_typed(values, ::Type{T}) where {T} + operation = _bounded_fold( + identity, min, typemax(T), _BoundedResult(), + _AllBoundedValues(), RejectInvalid(), RejectEmpty(), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_maximum_typed(values, ::Type{T}) where {T} + operation = _bounded_fold( + identity, max, typemin(T), _BoundedResult(), + _AllBoundedValues(), RejectInvalid(), RejectEmpty(), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_mean_typed(values, ::Type{T}, ::Type{R}) where {T,R} + operation = _bounded_fold( + _BoundedConvert{R}(), +, zero(R), _BoundedMeanResult{R}(), + _AllBoundedValues(), RejectInvalid(), FillEmpty(R(NaN)), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_geometric_mean_typed(values, ::Type{T}) where {T} + operation = _bounded_fold( + _BoundedGeometricMap(), +, zero(T), _BoundedGeometricResult{T}(), + Where(_CONSTRUCTION_TOKEN, _BoundedPositive()), RejectInvalid(), + RejectEmpty(), CanonicalLeftFold(), + ) + return operation(values) +end + +@generated function Base.sum(values::V) where {V<:_BoundedFoldInput} + T = _bounded_reduction_input_type(V) + T <: _BoundedPrimitiveNumber || + return _inline_bounded_reduction( + :(_unsupported_bounded_reduction(:sum, $T))) + R = _bounded_sum_type(T) + return _inline_bounded_reduction( + :(_bounded_sum_typed(values, $T, $R))) +end + +@generated function Base.minimum(values::V) where {V<:_BoundedFoldInput} + T = _bounded_reduction_input_type(V) + T <: _BoundedPrimitiveNumber || + return _inline_bounded_reduction( + :(_unsupported_bounded_reduction(:minimum, $T))) + return _inline_bounded_reduction( + :(_bounded_minimum_typed(values, $T))) +end + +@generated function Base.maximum(values::V) where {V<:_BoundedFoldInput} + T = _bounded_reduction_input_type(V) + T <: _BoundedPrimitiveNumber || + return _inline_bounded_reduction( + :(_unsupported_bounded_reduction(:maximum, $T))) + return _inline_bounded_reduction( + :(_bounded_maximum_typed(values, $T))) +end + +@generated function Statistics.mean(values::V) where {V<:_BoundedFoldInput} + T = _bounded_reduction_input_type(V) + T <: _BoundedPrimitiveNumber || + return _inline_bounded_reduction( + :(_unsupported_bounded_reduction(:mean, $T))) + R = _bounded_mean_type(T) + return _inline_bounded_reduction( + :(_bounded_mean_typed(values, $T, $R))) +end + +""" + LocalMath.geometric_mean(values) + +Compute the geometric mean of a bounded floating-point gather or Collection +group in canonical lane order. Absent lanes do not participate. A nonpositive +present value or empty input rejects the containing transaction. +""" +@generated function geometric_mean(values::V) where {V<:_BoundedFoldInput} + T = _bounded_reduction_input_type(V) + T <: Union{Float16,Float32,Float64} || + return _inline_bounded_reduction( + :(_unsupported_bounded_reduction(:geometric_mean, $T))) + return _inline_bounded_reduction( + :(_bounded_geometric_mean_typed(values, $T))) +end diff --git a/test/metal/localmath_authoring.jl b/test/metal/localmath_authoring.jl index d0a468f..cf3b93c 100644 --- a/test/metal/localmath_authoring.jl +++ b/test/metal/localmath_authoring.jl @@ -151,6 +151,41 @@ struct LocalMathMetalNode end @test Array(LocalMath.storage(rejected_fold, fold_output)) == Float32[-1, -1] + mean_output = LocalMath.Field(fold_sources, Float32) + geometric_output = LocalMath.Field(fold_sources, Float32) + mean_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + mean_output[item] = Statistics.mean(values) + end + geometric_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + geometric_output[item] = LocalMath.geometric_mean(values) + end + reductions = LocalMath.sequence(mean_law, geometric_law) + reductions_prepared = LocalMath.prepare(reductions, + fold_values => LocalMath.Allocate(Float32[1, 4, 4, 16]), + mean_output => LocalMath.Allocate(undef), + geometric_output => LocalMath.Allocate(undef), + neighborhoods => LocalMath.Allocate( + reshape(Int32[1, 2, 3, 4], 2, 2)); + backend) + wait(LocalMath.execute!(reductions_prepared)) + @test Array(LocalMath.storage(reductions_prepared, mean_output)) == + Float32[2.5, 10] + @test Array(LocalMath.storage(reductions_prepared, geometric_output)) == + Float32[2, 8] + + rejected_geometric = LocalMath.prepare(geometric_law, + fold_values => LocalMath.Allocate(Float32[1, 0, 4, 16]), + geometric_output => LocalMath.Allocate(Float32(-1)), + neighborhoods => LocalMath.Allocate( + reshape(Int32[1, 2, 3, 4], 2, 2)); + backend) + @test_throws LocalMath.LocalMathValidationError wait( + LocalMath.execute!(rejected_geometric)) + @test Array(LocalMath.storage(rejected_geometric, geometric_output)) == + Float32[-1, -1] + fresh = LocalMath.CompactedStorage( backend, Int32, 3; group_count = 2, source_items = 6, source_position = true) diff --git a/test/metal/runtests.jl b/test/metal/runtests.jl index 3e2753f..e7f011a 100644 --- a/test/metal/runtests.jl +++ b/test/metal/runtests.jl @@ -1,6 +1,7 @@ using KernelAbstractions using LocalMath using Metal +using Statistics using Test Metal.functional() || error("the selected Metal witness is not functional") diff --git a/test/runtests.jl b/test/runtests.jl index 06bb90a..467e72e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -36,6 +36,7 @@ const LOCALMATH_INCLUDED_TESTS = ( "test_storage_authoring.jl", "test_prepare_authoring.jl", "test_localmath_authoring.jl", + "test_bounded_reductions.jl", "test_semantic_oracles.jl", ) diff --git a/test/test_bounded_reductions.jl b/test/test_bounded_reductions.jl new file mode 100644 index 0000000..e41f7a6 --- /dev/null +++ b/test/test_bounded_reductions.jl @@ -0,0 +1,121 @@ +using Statistics + +const LMBR = LocalMath + +@testset "bounded reductions match ordinary Julia values and types" begin + for T in ( + Bool, + Int8, Int16, Int32, Int64, + UInt8, UInt16, UInt32, UInt64, + Float16, Float32, Float64, + ) + data = T === Bool ? Bool[true, false, true] : T[T(1), T(1), T(2)] + view = LMBR._bounded_group_view(data, Int32(1), Int32(3), Val(3)) + @test sum(view) == sum(data) + @test typeof(sum(view)) === typeof(sum(data)) + @test minimum(view) == minimum(data) + @test typeof(minimum(view)) === typeof(minimum(data)) + @test maximum(view) == maximum(data) + @test typeof(maximum(view)) === typeof(maximum(data)) + @test mean(view) == mean(data) + @test typeof(mean(view)) === typeof(mean(data)) + + empty_view = LMBR._bounded_group_view( + T[], Int32(1), Int32(0), Val(3)) + @test sum(empty_view) == sum(T[]) + @test typeof(sum(empty_view)) === typeof(sum(T[])) + @test isnan(mean(empty_view)) + @test typeof(mean(empty_view)) === typeof(mean(T[])) + end + + floating = Float32[1, 4, 16] + floating_view = LMBR._bounded_group_view( + floating, Int32(1), Int32(3), Val(3)) + geometric = LMBR.geometric_mean(floating_view) + @test geometric ≈ 4.0f0 + @test geometric isa Float32 + + exceptional = Float32[NaN, Inf, -0.0f0, 0.0f0] + exceptional_view = LMBR._bounded_group_view( + exceptional, Int32(1), Int32(4), Val(4)) + @test isnan(sum(exceptional_view)) + @test isnan(minimum(exceptional_view)) + @test isnan(maximum(exceptional_view)) +end + +@testset "authored bounded reductions use transaction validation" begin + source = LMBR.Space(1) + values_space = LMBR.Space(3) + values = LMBR.Field(values_space, Float32) + totals = LMBR.Field(source, Float32) + minima = LMBR.Field(source, Float32) + maxima = LMBR.Field(source, Float32) + averages = LMBR.Field(source, Float32) + neighbors = LMBR.FixedRelation(source => values_space; degree = 3) + sum_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + totals[item] = sum(gathered) + end + minimum_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + minima[item] = minimum(gathered) + end + maximum_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + maxima[item] = maximum(gathered) + end + mean_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + averages[item] = Statistics.mean(gathered) + end + law = LMBR.sequence(sum_law, minimum_law, maximum_law, mean_law) + prepared = LMBR.prepare( + law, + values => Float32[1, 2, 3], + totals => zeros(Float32, 1), + minima => zeros(Float32, 1), + maxima => zeros(Float32, 1), + averages => zeros(Float32, 1), + neighbors => reshape(Int32[1, 2, 3], 3, 1); + backend = KernelAbstractions.CPU(), + ) + wait(LMBR.execute!(prepared)) + @test LMBR.storage(prepared, totals) == Float32[6] + @test LMBR.storage(prepared, minima) == Float32[1] + @test LMBR.storage(prepared, maxima) == Float32[3] + @test LMBR.storage(prepared, averages) == Float32[2] + + keys = LMBR.Field(source, Int32) + optional = LMBR.IndexRelation(keys => values_space; optional = true) + rejected_output = LMBR.Field(source, Float32) + rejected_law = LMBR.@localmath item ∈ source begin + gathered = samples(values[optional(item)]) + rejected_output[item] = minimum(gathered) + end + rejected = LMBR.prepare( + rejected_law, + keys => Int32[0], + values => Float32[1, 2, 3], + rejected_output => Float32[-1]; + backend = KernelAbstractions.CPU(), + ) + @test_throws LMBR.LocalMathValidationError wait(LMBR.execute!(rejected)) + @test LMBR.storage(rejected, rejected_output) == Float32[-1] + + floating = LMBR.Field(values_space, Float32) + geometric_output = LMBR.Field(source, Float32) + geometric_law = LMBR.@localmath item ∈ source begin + gathered = floating[neighbors(item)] + geometric_output[item] = LMBR.geometric_mean(gathered) + end + geometric_rejected = LMBR.prepare( + geometric_law, + floating => Float32[1, 0, 4], + geometric_output => Float32[-1], + neighbors => reshape(Int32[1, 2, 3], 3, 1); + backend = KernelAbstractions.CPU(), + ) + @test_throws LMBR.LocalMathValidationError wait( + LMBR.execute!(geometric_rejected)) + @test LMBR.storage(geometric_rejected, geometric_output) == Float32[-1] +end diff --git a/test/test_public_api.jl b/test/test_public_api.jl index 91390ca..47f7a07 100644 --- a/test/test_public_api.jl +++ b/test/test_public_api.jl @@ -38,7 +38,7 @@ :TieMin, :TieMax, :RejectOverflow, :EmptyCollection, :FoldComponent, :InitializedState, :initialized_state, :BoundedWrites, :FoldStep, - :BoundedFold, :fold, :Where, + :BoundedFold, :fold, :geometric_mean, :Where, :RejectInvalid, :SkipInvalid, :FillInvalid, :RejectEmpty, :RelaxedAssociative, :BoundedFoldOutcome, :evaluate_bounded, :UniqueValue, :ConditionalUniqueValue, :RoutedUniqueValue, From 1794ce26a5599b3e267717c9756d7b8bf0eee671 Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Sun, 6 Sep 2026 20:04:24 -0400 Subject: [PATCH 2/5] Inline bounded reductions on device --- src/bounded_reductions.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bounded_reductions.jl b/src/bounded_reductions.jl index 34a4fbd..b0e4a8a 100644 --- a/src/bounded_reductions.jl +++ b/src/bounded_reductions.jl @@ -99,7 +99,7 @@ end return operation(values) end -@generated function Base.sum(values::V) where {V<:_BoundedFoldInput} +@inline @generated function Base.sum(values::V) where {V<:_BoundedFoldInput} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -109,7 +109,7 @@ end :(_bounded_sum_typed(values, $T, $R))) end -@generated function Base.minimum(values::V) where {V<:_BoundedFoldInput} +@inline @generated function Base.minimum(values::V) where {V<:_BoundedFoldInput} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -118,7 +118,7 @@ end :(_bounded_minimum_typed(values, $T))) end -@generated function Base.maximum(values::V) where {V<:_BoundedFoldInput} +@inline @generated function Base.maximum(values::V) where {V<:_BoundedFoldInput} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -127,7 +127,7 @@ end :(_bounded_maximum_typed(values, $T))) end -@generated function Statistics.mean(values::V) where {V<:_BoundedFoldInput} +@inline @generated function Statistics.mean(values::V) where {V<:_BoundedFoldInput} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -144,7 +144,7 @@ Compute the geometric mean of a bounded floating-point gather or Collection group in canonical lane order. Absent lanes do not participate. A nonpositive present value or empty input rejects the containing transaction. """ -@generated function geometric_mean(values::V) where {V<:_BoundedFoldInput} +@inline @generated function geometric_mean(values::V) where {V<:_BoundedFoldInput} T = _bounded_reduction_input_type(V) T <: Union{Float16,Float32,Float64} || return _inline_bounded_reduction( From 9527788aafdfb24daa7a3a2c356a106da0fde1dc Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Sun, 6 Sep 2026 20:41:04 -0400 Subject: [PATCH 3/5] Qualify bounded reductions on Metal --- src/bounded_reductions.jl | 54 +++++++++++++++++-- .../stage_program_kernelabstractions.jl | 8 ++- test/metal/localmath_authoring.jl | 1 + 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/bounded_reductions.jl b/src/bounded_reductions.jl index b0e4a8a..9211522 100644 --- a/src/bounded_reductions.jl +++ b/src/bounded_reductions.jl @@ -99,7 +99,7 @@ end return operation(values) end -@inline @generated function Base.sum(values::V) where {V<:_BoundedFoldInput} +function _bounded_sum_expression(::Type{V}) where {V} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -109,7 +109,7 @@ end :(_bounded_sum_typed(values, $T, $R))) end -@inline @generated function Base.minimum(values::V) where {V<:_BoundedFoldInput} +function _bounded_minimum_expression(::Type{V}) where {V} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -118,7 +118,7 @@ end :(_bounded_minimum_typed(values, $T))) end -@inline @generated function Base.maximum(values::V) where {V<:_BoundedFoldInput} +function _bounded_maximum_expression(::Type{V}) where {V} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -127,7 +127,7 @@ end :(_bounded_maximum_typed(values, $T))) end -@inline @generated function Statistics.mean(values::V) where {V<:_BoundedFoldInput} +function _bounded_mean_expression(::Type{V}) where {V} T = _bounded_reduction_input_type(V) T <: _BoundedPrimitiveNumber || return _inline_bounded_reduction( @@ -137,6 +137,33 @@ end :(_bounded_mean_typed(values, $T, $R))) end +for (operation, expression) in ( + (:sum, :_bounded_sum_expression), + (:minimum, :_bounded_minimum_expression), + (:maximum, :_bounded_maximum_expression), + (:mean, :_bounded_mean_expression), + ) + owner = operation === :mean ? Statistics : Base + @eval begin + @inline @generated function $owner.$operation( + values::_StageRead{F,R,V}) where {F,R,V} + return $expression(_StageRead{F,R,V}) + end + @inline @generated function $owner.$operation( + values::_AuthoringValues{R}) where {R} + return $expression(_AuthoringValues{R}) + end + @inline @generated function $owner.$operation( + values::_AuthoringSamples{R}) where {R} + return $expression(_AuthoringSamples{R}) + end + @inline @generated function $owner.$operation( + values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} + return $expression(BoundedGroupView{K,T,R,V}) + end + end +end + """ LocalMath.geometric_mean(values) @@ -144,7 +171,7 @@ Compute the geometric mean of a bounded floating-point gather or Collection group in canonical lane order. Absent lanes do not participate. A nonpositive present value or empty input rejects the containing transaction. """ -@inline @generated function geometric_mean(values::V) where {V<:_BoundedFoldInput} +function _bounded_geometric_mean_expression(::Type{V}) where {V} T = _bounded_reduction_input_type(V) T <: Union{Float16,Float32,Float64} || return _inline_bounded_reduction( @@ -152,3 +179,20 @@ present value or empty input rejects the containing transaction. return _inline_bounded_reduction( :(_bounded_geometric_mean_typed(values, $T))) end + +@inline @generated function geometric_mean( + values::_StageRead{F,R,V}) where {F,R,V} + return _bounded_geometric_mean_expression(_StageRead{F,R,V}) +end +@inline @generated function geometric_mean( + values::_AuthoringValues{R}) where {R} + return _bounded_geometric_mean_expression(_AuthoringValues{R}) +end +@inline @generated function geometric_mean( + values::_AuthoringSamples{R}) where {R} + return _bounded_geometric_mean_expression(_AuthoringSamples{R}) +end +@inline @generated function geometric_mean( + values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} + return _bounded_geometric_mean_expression(BoundedGroupView{K,T,R,V}) +end diff --git a/src/execution/stage_program_kernelabstractions.jl b/src/execution/stage_program_kernelabstractions.jl index dce16fc..a8e6922 100644 --- a/src/execution/stage_program_kernelabstractions.jl +++ b/src/execution/stage_program_kernelabstractions.jl @@ -312,6 +312,11 @@ function _pointwise_source_safe(value) # optimized typed IR below must inline them to the same closed set of # load-only primitives; an opaque user invoke remains rejected. binding isa Function && return true + # Hygienic authored code retains qualified calls such as + # `Statistics.mean`. Admit the constant module name only at this + # source pass; the typed-IR pass must still resolve the selected + # function to the same closed, load-only call graph. + binding isa Module && return true binding isa Type && parentmodule(binding) in (Base, Core) && return true binding isa DataType && isconcretetype(binding) && isbitstype(binding) && _storage_free_type(binding) && return true @@ -421,8 +426,9 @@ function _pointwise_method_instance_safe( # contract makes these two compiler entry points an explicit cold-boundary # dependency. Runtime execution never consumes this inspection result. depth < _POINTWISE_CALL_DEPTH_LIMIT || return false - haskey(analysis.memo, method_instance) && + if haskey(analysis.memo, method_instance) return analysis.memo[method_instance] + end method_instance in analysis.active && return false spec = Base.unwrap_unionall(method_instance.specTypes) spec isa DataType && spec <: Tuple || return false diff --git a/test/metal/localmath_authoring.jl b/test/metal/localmath_authoring.jl index cf3b93c..9efe4e1 100644 --- a/test/metal/localmath_authoring.jl +++ b/test/metal/localmath_authoring.jl @@ -2,6 +2,7 @@ using Test using Metal using LocalMath using StaticArrays +using Statistics struct LocalMathMetalNode end From fd522203c17551343f7c1a38c4e95120eb34c278 Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Sun, 6 Sep 2026 20:50:35 -0400 Subject: [PATCH 4/5] Make bounded reduction methods GPU-static --- src/bounded_reductions.jl | 95 ++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/src/bounded_reductions.jl b/src/bounded_reductions.jl index 9211522..c01241c 100644 --- a/src/bounded_reductions.jl +++ b/src/bounded_reductions.jl @@ -137,40 +137,66 @@ function _bounded_mean_expression(::Type{V}) where {V} :(_bounded_mean_typed(values, $T, $R))) end -for (operation, expression) in ( - (:sum, :_bounded_sum_expression), - (:minimum, :_bounded_minimum_expression), - (:maximum, :_bounded_maximum_expression), - (:mean, :_bounded_mean_expression), - ) - owner = operation === :mean ? Statistics : Base - @eval begin - @inline @generated function $owner.$operation( - values::_StageRead{F,R,V}) where {F,R,V} - return $expression(_StageRead{F,R,V}) - end - @inline @generated function $owner.$operation( - values::_AuthoringValues{R}) where {R} - return $expression(_AuthoringValues{R}) - end - @inline @generated function $owner.$operation( - values::_AuthoringSamples{R}) where {R} - return $expression(_AuthoringSamples{R}) - end - @inline @generated function $owner.$operation( - values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} - return $expression(BoundedGroupView{K,T,R,V}) - end - end +@inline @generated function Base.sum( + values::_StageRead{F,R,V}) where {F,R,V} + return _bounded_sum_expression(_StageRead{F,R,V}) +end +@inline @generated function Base.sum(values::_AuthoringValues{R}) where {R} + return _bounded_sum_expression(_AuthoringValues{R}) +end +@inline @generated function Base.sum(values::_AuthoringSamples{R}) where {R} + return _bounded_sum_expression(_AuthoringSamples{R}) +end +@inline @generated function Base.sum( + values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} + return _bounded_sum_expression(BoundedGroupView{K,T,R,V}) end -""" - LocalMath.geometric_mean(values) +@inline @generated function Base.minimum( + values::_StageRead{F,R,V}) where {F,R,V} + return _bounded_minimum_expression(_StageRead{F,R,V}) +end +@inline @generated function Base.minimum(values::_AuthoringValues{R}) where {R} + return _bounded_minimum_expression(_AuthoringValues{R}) +end +@inline @generated function Base.minimum(values::_AuthoringSamples{R}) where {R} + return _bounded_minimum_expression(_AuthoringSamples{R}) +end +@inline @generated function Base.minimum( + values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} + return _bounded_minimum_expression(BoundedGroupView{K,T,R,V}) +end + +@inline @generated function Base.maximum( + values::_StageRead{F,R,V}) where {F,R,V} + return _bounded_maximum_expression(_StageRead{F,R,V}) +end +@inline @generated function Base.maximum(values::_AuthoringValues{R}) where {R} + return _bounded_maximum_expression(_AuthoringValues{R}) +end +@inline @generated function Base.maximum(values::_AuthoringSamples{R}) where {R} + return _bounded_maximum_expression(_AuthoringSamples{R}) +end +@inline @generated function Base.maximum( + values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} + return _bounded_maximum_expression(BoundedGroupView{K,T,R,V}) +end + +@inline @generated function Statistics.mean( + values::_StageRead{F,R,V}) where {F,R,V} + return _bounded_mean_expression(_StageRead{F,R,V}) +end +@inline @generated function Statistics.mean(values::_AuthoringValues{R}) where {R} + return _bounded_mean_expression(_AuthoringValues{R}) +end +@inline @generated function Statistics.mean(values::_AuthoringSamples{R}) where {R} + return _bounded_mean_expression(_AuthoringSamples{R}) +end +@inline @generated function Statistics.mean( + values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} + return _bounded_mean_expression(BoundedGroupView{K,T,R,V}) +end -Compute the geometric mean of a bounded floating-point gather or Collection -group in canonical lane order. Absent lanes do not participate. A nonpositive -present value or empty input rejects the containing transaction. -""" function _bounded_geometric_mean_expression(::Type{V}) where {V} T = _bounded_reduction_input_type(V) T <: Union{Float16,Float32,Float64} || @@ -180,6 +206,13 @@ function _bounded_geometric_mean_expression(::Type{V}) where {V} :(_bounded_geometric_mean_typed(values, $T))) end +""" + LocalMath.geometric_mean(values) + +Compute the geometric mean of a bounded floating-point gather or Collection +group in canonical lane order. Absent lanes do not participate. A nonpositive +present value or empty input rejects the containing transaction. +""" @inline @generated function geometric_mean( values::_StageRead{F,R,V}) where {F,R,V} return _bounded_geometric_mean_expression(_StageRead{F,R,V}) From f83770ee23a6ebf467adb04f63211d28af8322e8 Mon Sep 17 00:00:00 2001 From: PraneethMerugu Date: Sun, 6 Sep 2026 21:49:13 -0400 Subject: [PATCH 5/5] Make bounded reduction element types explicit --- src/authoring.jl | 6 +- src/bounded_reductions.jl | 173 ++++++++--------------------- src/execution/candidate_stage.jl | 21 +++- src/execution/stage_preparation.jl | 30 +++-- test/metal/localmath_authoring.jl | 27 ++++- test/test_stage_preparation.jl | 13 +-- 6 files changed, 114 insertions(+), 156 deletions(-) diff --git a/src/authoring.jl b/src/authoring.jl index 2e0c0fc..4f050f3 100644 --- a/src/authoring.jl +++ b/src/authoring.jl @@ -3,10 +3,10 @@ include("authoring/preparation_syntax.jl") # Evaluator-side facades. Their only payload is the existing bounded read # capability, so they add no semantic or executable representation. -struct _AuthoringValues{R} +struct _AuthoringValues{T,R} read::R end -struct _AuthoringSamples{R} +struct _AuthoringSamples{T,R} read::R end struct _AuthoringIndices{R} @@ -30,8 +30,6 @@ _device_evaluator_capture(operation::_ValidationAuthoringEvaluator) = _contains_bounded_fold_type( ::Type{<:_ValidationAuthoringEvaluator}, seen = IdSet{Any}()) = true -@inline _authoring_values(read) = _AuthoringValues(read) -@inline _authoring_samples(read) = _AuthoringSamples(read) @inline _authoring_indices(read) = _AuthoringIndices(read) @inline Base.length(view::Union{ diff --git a/src/bounded_reductions.jl b/src/bounded_reductions.jl index c01241c..4745984 100644 --- a/src/bounded_reductions.jl +++ b/src/bounded_reductions.jl @@ -33,14 +33,6 @@ _bounded_sum_type(::Type{T}) where {T<:AbstractFloat} = T _bounded_mean_type(::Type{T}) where {T<:Integer} = Float64 _bounded_mean_type(::Type{T}) where {T<:AbstractFloat} = T -function _bounded_reduction_input_type(::Type{V}) where {V} - sample_type = Core.Compiler.return_type(getindex, Tuple{V,Int}) - if sample_type isa DataType && sample_type <: _StageSample - return Base.unwrap_unionall(sample_type).parameters[1] - end - return sample_type -end - function _unsupported_bounded_reduction(name::Symbol, ::Type{T}) where {T} throw(LocalMathValidationError( "$(name) does not support this bounded value type"; @@ -51,9 +43,6 @@ function _unsupported_bounded_reduction(name::Symbol, ::Type{T}) where {T} )) end -_inline_bounded_reduction(expression) = - Expr(:block, Expr(:meta, :inline), expression) - @inline function _bounded_sum_typed(values, ::Type{T}, ::Type{R}) where {T,R} operation = _bounded_fold( _BoundedConvert{R}(), +, zero(R), _BoundedResult(), @@ -99,112 +88,50 @@ end return operation(values) end -function _bounded_sum_expression(::Type{V}) where {V} - T = _bounded_reduction_input_type(V) - T <: _BoundedPrimitiveNumber || - return _inline_bounded_reduction( - :(_unsupported_bounded_reduction(:sum, $T))) - R = _bounded_sum_type(T) - return _inline_bounded_reduction( - :(_bounded_sum_typed(values, $T, $R))) -end +@inline _bounded_sum(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_sum_typed(values, T, _bounded_sum_type(T)) +@inline _bounded_sum(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:sum, T) -function _bounded_minimum_expression(::Type{V}) where {V} - T = _bounded_reduction_input_type(V) - T <: _BoundedPrimitiveNumber || - return _inline_bounded_reduction( - :(_unsupported_bounded_reduction(:minimum, $T))) - return _inline_bounded_reduction( - :(_bounded_minimum_typed(values, $T))) -end +@inline _bounded_minimum(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_minimum_typed(values, T) +@inline _bounded_minimum(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:minimum, T) -function _bounded_maximum_expression(::Type{V}) where {V} - T = _bounded_reduction_input_type(V) - T <: _BoundedPrimitiveNumber || - return _inline_bounded_reduction( - :(_unsupported_bounded_reduction(:maximum, $T))) - return _inline_bounded_reduction( - :(_bounded_maximum_typed(values, $T))) -end +@inline _bounded_maximum(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_maximum_typed(values, T) +@inline _bounded_maximum(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:maximum, T) -function _bounded_mean_expression(::Type{V}) where {V} - T = _bounded_reduction_input_type(V) - T <: _BoundedPrimitiveNumber || - return _inline_bounded_reduction( - :(_unsupported_bounded_reduction(:mean, $T))) - R = _bounded_mean_type(T) - return _inline_bounded_reduction( - :(_bounded_mean_typed(values, $T, $R))) -end +@inline _bounded_mean(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_mean_typed(values, T, _bounded_mean_type(T)) +@inline _bounded_mean(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:mean, T) -@inline @generated function Base.sum( - values::_StageRead{F,R,V}) where {F,R,V} - return _bounded_sum_expression(_StageRead{F,R,V}) -end -@inline @generated function Base.sum(values::_AuthoringValues{R}) where {R} - return _bounded_sum_expression(_AuthoringValues{R}) -end -@inline @generated function Base.sum(values::_AuthoringSamples{R}) where {R} - return _bounded_sum_expression(_AuthoringSamples{R}) -end -@inline @generated function Base.sum( - values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} - return _bounded_sum_expression(BoundedGroupView{K,T,R,V}) -end - -@inline @generated function Base.minimum( - values::_StageRead{F,R,V}) where {F,R,V} - return _bounded_minimum_expression(_StageRead{F,R,V}) -end -@inline @generated function Base.minimum(values::_AuthoringValues{R}) where {R} - return _bounded_minimum_expression(_AuthoringValues{R}) -end -@inline @generated function Base.minimum(values::_AuthoringSamples{R}) where {R} - return _bounded_minimum_expression(_AuthoringSamples{R}) -end -@inline @generated function Base.minimum( - values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} - return _bounded_minimum_expression(BoundedGroupView{K,T,R,V}) -end - -@inline @generated function Base.maximum( - values::_StageRead{F,R,V}) where {F,R,V} - return _bounded_maximum_expression(_StageRead{F,R,V}) -end -@inline @generated function Base.maximum(values::_AuthoringValues{R}) where {R} - return _bounded_maximum_expression(_AuthoringValues{R}) -end -@inline @generated function Base.maximum(values::_AuthoringSamples{R}) where {R} - return _bounded_maximum_expression(_AuthoringSamples{R}) -end -@inline @generated function Base.maximum( - values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} - return _bounded_maximum_expression(BoundedGroupView{K,T,R,V}) -end - -@inline @generated function Statistics.mean( - values::_StageRead{F,R,V}) where {F,R,V} - return _bounded_mean_expression(_StageRead{F,R,V}) -end -@inline @generated function Statistics.mean(values::_AuthoringValues{R}) where {R} - return _bounded_mean_expression(_AuthoringValues{R}) -end -@inline @generated function Statistics.mean(values::_AuthoringSamples{R}) where {R} - return _bounded_mean_expression(_AuthoringSamples{R}) -end -@inline @generated function Statistics.mean( - values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} - return _bounded_mean_expression(BoundedGroupView{K,T,R,V}) +for (operation, helper) in ( + (:sum, :_bounded_sum), + (:minimum, :_bounded_minimum), + (:maximum, :_bounded_maximum), + (:mean, :_bounded_mean), + ) + owner = operation === :mean ? Statistics : Base + @eval begin + @inline $owner.$operation(values::_StageRead{T}) where {T} = + $helper(values, T) + @inline $owner.$operation(values::_AuthoringValues{T}) where {T} = + $helper(values, T) + @inline $owner.$operation(values::_AuthoringSamples{T}) where {T} = + $helper(values, T) + @inline $owner.$operation(values::BoundedGroupView{K,T}) where {K,T} = + $helper(values, T) + end end -function _bounded_geometric_mean_expression(::Type{V}) where {V} - T = _bounded_reduction_input_type(V) - T <: Union{Float16,Float32,Float64} || - return _inline_bounded_reduction( - :(_unsupported_bounded_reduction(:geometric_mean, $T))) - return _inline_bounded_reduction( - :(_bounded_geometric_mean_typed(values, $T))) -end +@inline _bounded_geometric_mean( + values, ::Type{T}) where {T<:Union{Float16,Float32,Float64}} = + _bounded_geometric_mean_typed(values, T) +@inline _bounded_geometric_mean(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:geometric_mean, T) """ LocalMath.geometric_mean(values) @@ -213,19 +140,11 @@ Compute the geometric mean of a bounded floating-point gather or Collection group in canonical lane order. Absent lanes do not participate. A nonpositive present value or empty input rejects the containing transaction. """ -@inline @generated function geometric_mean( - values::_StageRead{F,R,V}) where {F,R,V} - return _bounded_geometric_mean_expression(_StageRead{F,R,V}) -end -@inline @generated function geometric_mean( - values::_AuthoringValues{R}) where {R} - return _bounded_geometric_mean_expression(_AuthoringValues{R}) -end -@inline @generated function geometric_mean( - values::_AuthoringSamples{R}) where {R} - return _bounded_geometric_mean_expression(_AuthoringSamples{R}) -end -@inline @generated function geometric_mean( - values::BoundedGroupView{K,T,R,V}) where {K,T,R,V} - return _bounded_geometric_mean_expression(BoundedGroupView{K,T,R,V}) -end +@inline geometric_mean(values::_StageRead{T}) where {T} = + _bounded_geometric_mean(values, T) +@inline geometric_mean(values::_AuthoringValues{T}) where {T} = + _bounded_geometric_mean(values, T) +@inline geometric_mean(values::_AuthoringSamples{T}) where {T} = + _bounded_geometric_mean(values, T) +@inline geometric_mean(values::BoundedGroupView{K,T}) where {K,T} = + _bounded_geometric_mean(values, T) diff --git a/src/execution/candidate_stage.jl b/src/execution/candidate_stage.jl index 1c6e5dd..3f5d547 100644 --- a/src/execution/candidate_stage.jl +++ b/src/execution/candidate_stage.jl @@ -571,11 +571,22 @@ end result, fields, item, port + 1, execution) end -@inline _stage_read(stage, access::_PreparedStageAccess, item::Int32) = - _StageRead(stage.fields, access.relation, item, _NoEvaluationValidation()) -@inline _stage_read(stage, access::_PreparedStageAccess, item::Int32, - validation) = - _StageRead(stage.fields, access.relation, item, validation) +@inline function _stage_read( + stage, access::_PreparedStageAccess{T}, item::Int32) where {T} + fields = stage.fields + relation = access.relation + validation = _NoEvaluationValidation() + return _StageRead{T,typeof(fields),typeof(relation),typeof(validation)}( + fields, relation, item, validation) +end +@inline function _stage_read( + stage, access::_PreparedStageAccess{T}, item::Int32, + validation) where {T} + fields = stage.fields + relation = access.relation + return _StageRead{T,typeof(fields),typeof(relation),typeof(validation)}( + fields, relation, item, validation) +end @inline function _stage_read(stage, access::_PreparedCollectionAccess{<:_BoundedGroup{K}}, item::Int32 ) where {K} diff --git a/src/execution/stage_preparation.jl b/src/execution/stage_preparation.jl index ec16766..394471c 100644 --- a/src/execution/stage_preparation.jl +++ b/src/execution/stage_preparation.jl @@ -1,12 +1,17 @@ # The one bridge from a cold Stage projection to the descriptor-free warm ABI. -struct _StageRead{F,R,V} +struct _StageRead{T,F,R,V} fields::F relation::R item::Int32 validation::V end +@inline _authoring_values(read::_StageRead{T}) where {T} = + _AuthoringValues{T,typeof(read)}(read) +@inline _authoring_samples(read::_StageRead{T}) where {T} = + _AuthoringSamples{T,typeof(read)}(read) + # Effect admission is intentionally performed against a host-compilable # surrogate signature. Array storage can be nested below the package-owned # bounded-read capability (rather than appearing as a top-level argument), so @@ -45,8 +50,9 @@ end _pointwise_surrogate_type(::Type{T}) where {T<:_StageSurrogateContainer} = _stage_surrogate_container_type(T) -function _pointwise_surrogate_type(::Type{_StageRead{F,R,V}}) where {F,R,V} +function _pointwise_surrogate_type(::Type{_StageRead{T,F,R,V}}) where {T,F,R,V} return _StageRead{ + T, _pointwise_surrogate_type(F), _pointwise_surrogate_type(R), V, @@ -209,7 +215,7 @@ function _validate_stage_read_methods(signature) return all(_package_owned_stage_read_protocol, reads.parameters) end -struct _PreparedStageAccess{R}; relation::R; end +struct _PreparedStageAccess{T,R}; relation::R; end struct _PreparedCollectionAccess{L,S}; law::L; storage::S; end struct _PreparedStageComponent{R}; relation::R; end struct _PreparedStageCollection{S}; storage::S; end @@ -280,7 +286,10 @@ struct _PreparedFieldGate{S<:_PreparedFieldSlot}; slot::S; end struct _PreparedStageControl{P,M,S,G} prefix::P; mask::M; subset::S; gate::G end -Adapt.@adapt_structure _PreparedStageAccess +function Adapt.adapt_structure(to, access::_PreparedStageAccess{T}) where {T} + relation = Adapt.adapt(to, access.relation) + return _PreparedStageAccess{T,typeof(relation)}(relation) +end Adapt.@adapt_structure _PreparedCollectionAccess Adapt.@adapt_structure _PreparedStageComponent Adapt.@adapt_structure _PreparedStageCollection @@ -495,8 +504,13 @@ function _projected_relation_view(validated::_ValidatedStructuralBinding, return _PreparedRelationUse(view, binding.generation, binding.status) end -_prepare_stage_access(validated, layout, use::_ProjectedRelationUse) = - _PreparedStageAccess(_projected_relation_view(validated, layout, use)) +function _prepare_stage_access(validated, layout, + use::_ProjectedRelationUse{R,<:_PreparedFieldSlot{I}}) where {R,I} + field_slot = getfield(layout.fields, I) + T = eltype(_validated_field_binding(validated, field_slot).storage) + relation = _projected_relation_view(validated, layout, use) + return _PreparedStageAccess{T,typeof(relation)}(relation) +end _prepare_stage_access(validated, layout, use::_ProjectedCollectionAccess) = _PreparedCollectionAccess(use.law, _collection_binding(validated, use.slot).storage) @@ -957,8 +971,8 @@ Base.@nospecializeinfer Base.@noinline function _stage_draft_from_projection( end -_stage_read_type(fields::Tuple, access::_PreparedStageAccess) = - Core.apply_type(_StageRead, typeof(fields), typeof(access.relation), +_stage_read_type(fields::Tuple, access::_PreparedStageAccess{T}) where {T} = + Core.apply_type(_StageRead, T, typeof(fields), typeof(access.relation), _NoEvaluationValidation) function _stage_read_type(fields::Tuple, access::_PreparedCollectionAccess{<:_BoundedGroup{K}}) where {K} diff --git a/test/metal/localmath_authoring.jl b/test/metal/localmath_authoring.jl index 9efe4e1..c09d535 100644 --- a/test/metal/localmath_authoring.jl +++ b/test/metal/localmath_authoring.jl @@ -152,8 +152,23 @@ struct LocalMathMetalNode end @test Array(LocalMath.storage(rejected_fold, fold_output)) == Float32[-1, -1] + sum_output = LocalMath.Field(fold_sources, Float32) + minimum_output = LocalMath.Field(fold_sources, Float32) + maximum_output = LocalMath.Field(fold_sources, Float32) mean_output = LocalMath.Field(fold_sources, Float32) geometric_output = LocalMath.Field(fold_sources, Float32) + sum_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + sum_output[item] = sum(values) + end + minimum_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + minimum_output[item] = minimum(values) + end + maximum_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + maximum_output[item] = maximum(values) + end mean_law = LocalMath.@localmath item ∈ fold_sources begin values = fold_values[neighborhoods(item)] mean_output[item] = Statistics.mean(values) @@ -162,15 +177,25 @@ struct LocalMathMetalNode end values = fold_values[neighborhoods(item)] geometric_output[item] = LocalMath.geometric_mean(values) end - reductions = LocalMath.sequence(mean_law, geometric_law) + reductions = LocalMath.sequence( + sum_law, minimum_law, maximum_law, mean_law, geometric_law) reductions_prepared = LocalMath.prepare(reductions, fold_values => LocalMath.Allocate(Float32[1, 4, 4, 16]), + sum_output => LocalMath.Allocate(undef), + minimum_output => LocalMath.Allocate(undef), + maximum_output => LocalMath.Allocate(undef), mean_output => LocalMath.Allocate(undef), geometric_output => LocalMath.Allocate(undef), neighborhoods => LocalMath.Allocate( reshape(Int32[1, 2, 3, 4], 2, 2)); backend) wait(LocalMath.execute!(reductions_prepared)) + @test Array(LocalMath.storage(reductions_prepared, sum_output)) == + Float32[5, 20] + @test Array(LocalMath.storage(reductions_prepared, minimum_output)) == + Float32[1, 4] + @test Array(LocalMath.storage(reductions_prepared, maximum_output)) == + Float32[4, 16] @test Array(LocalMath.storage(reductions_prepared, mean_output)) == Float32[2.5, 10] @test Array(LocalMath.storage(reductions_prepared, geometric_output)) == diff --git a/test/test_stage_preparation.jl b/test/test_stage_preparation.jl index 89c0c3e..cd31926 100644 --- a/test/test_stage_preparation.jl +++ b/test/test_stage_preparation.jl @@ -194,10 +194,7 @@ end @test !(prepared.accesses[1].relation.view isa LMPRE.Relation) @test !(prepared.publications[1].components[1].relation.view isa LMPRE.Relation) - read = LMPRE._StageRead( - prepared.fields, prepared.accesses[1].relation, Int32(2), - LMPRE._NoEvaluationValidation(), - ) + read = LMPRE._stage_read(prepared, prepared.accesses[1], Int32(2)) @test length(read) == 1 @test read[1].value == 2.0f0 @test read[1].present @@ -205,13 +202,7 @@ end @test prepared.evaluator isa LMPRE._PortProjector{PreparedStageEvaluator} @test admission.result_type === NamedTuple{(:value,),Tuple{LMPRE.UniqueValue{Float32}}} - @test admission.signature == Tuple{ - Int32, - Tuple{LMPRE._StageRead{ - typeof(prepared.fields),typeof(prepared.accesses[1].relation), - LMPRE._NoEvaluationValidation}}, - Tuple{Float32}, - } + @test admission.signature == Tuple{Int32,Tuple{typeof(read)},Tuple{Float32}} @test !hasproperty(prepared, :backend) @test !hasproperty(prepared, :signature) @test LMPRE._stage_evaluator_parameters(