Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name = "LocalMath"
uuid = "82808884-8acc-4a5c-a056-be440a0fbea1"
version = "0.2.0-rc1"
version = "0.2.0-rc2"
authors = ["Praneeth Merugu <merugufam@gmail.com>"]

[deps]
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"

Expand All @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
18 changes: 18 additions & 0 deletions docs/src/api/localmath.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/LocalMath.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,6 +51,7 @@ import Adapt
import Atomix
import KernelAbstractions
import StaticArrays
import Statistics
import StructArrays
import UUIDs
using KernelAbstractions: @index, @kernel, @localmem, @synchronize
Expand All @@ -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")
Expand Down
6 changes: 2 additions & 4 deletions src/authoring.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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{
Expand Down
10 changes: 7 additions & 3 deletions src/authoring/syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
150 changes: 150 additions & 0 deletions src/bounded_reductions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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 _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 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

@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)

@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)

@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)

@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)

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

@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)

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 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)
21 changes: 16 additions & 5 deletions src/execution/candidate_stage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
30 changes: 22 additions & 8 deletions src/execution/stage_preparation.jl
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}
Expand Down
Loading