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
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
`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.
- Adds a device-safe computed-validity flag and bounded diagnostic witness to
`FoldStep`, preserving atomic ordered-state publication on semantic failure.

# LocalMath 0.2.0-rc1

Expand Down
8 changes: 8 additions & 0 deletions docs/src/api/localmath.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ destination. This holds for parameter gates and gates produced by a preceding
total Field publication. Opening the gate restores ordinary ordered-fold
validation, including rejection of duplicate ordering identities.

An ordered transition returns `FoldStep(updates; valid, witness, halt)`. A
computed `valid=false` result rejects before that step changes private
accumulator scratch and reports its `Int32` witness together with the source
item and canonical position. The stage publishes no accumulator component when
any step is invalid. Ordinary scientific denial is not a validation failure:
return a valid step that records the denied disposition and omits the denied
state change.

## Bounded scalar operators

`LocalMath.fold` names the mathematical action and takes an already bounded
Expand Down
7 changes: 7 additions & 0 deletions spec/localmath.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ Execution evolves ordered state in package-owned workspace and commits it only
after final validation succeeds, preserving stage-entry destinations on any
late recurrence failure.

The explicit transition API may return
`FoldStep(updates; valid=false, witness=Int32(...))` when its computed result
violates a domain-owned invariant. The executor reports that bounded witness
with the event's source item and canonical position before applying the step's
updates. A normal denied event remains a valid step: it records the denial in
state and emits zero writes for changes that were not realized.

## Binding and storage ownership

Ordinary preparation accepts one flat sequence of descriptor-to-storage pairs.
Expand Down
9 changes: 8 additions & 1 deletion src/execution/ordered_fold_stage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,14 @@ end
end
)
end
return Expr(:block, checks..., :((Int32(0), Int32(0), Int32(0))))
return Expr(
:block,
:(!step.valid && return (
Int32(_ORDERED_FOLD_INVALID_STEP), Int32(0), step.witness,
)),
checks...,
:((Int32(0), Int32(0), Int32(0))),
)
end
@inline function _ordered_fold_stage_apply_writes!(storage, writes::BoundedWrites)
for j in Int32(1):writes.count
Expand Down
9 changes: 7 additions & 2 deletions src/execution/stage_preparation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,13 @@ Base.eltype(::Type{_PreparedFoldRead{T}}) where {T} = T
Base.length(view::_PreparedFoldRead) = length(getfield(view, :storage))
Base.size(view::_PreparedFoldRead) = size(getfield(view, :storage))
Base.axes(view::_PreparedFoldRead) = axes(getfield(view, :storage))
@inline Base.getindex(view::_PreparedFoldRead, indices...) =
@inbounds getfield(view, :storage)[indices...]
# This call boundary preserves evolving-state loads across writes to the same
# ordered-fold scratch. Without it Metal hoists constant-index loads out of the
# recurrence and later canonical steps observe the initializer snapshot.
Base.@noinline Base.getindex(view::_PreparedFoldRead, index::Int) =
@inbounds getfield(view, :storage)[index]
Base.@noinline Base.getindex(view::_PreparedFoldRead, index::Int32) =
@inbounds getfield(view, :storage)[index]

struct _PreparedFoldAccumulatorView{Names,C<:NamedTuple}
components::C
Expand Down
1 change: 1 addition & 0 deletions src/execution/stage_program.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,7 @@ function _validated_publication_error(
code == _ORDERED_FOLD_DUPLICATE_ORDER ? :duplicate_order_identity :
code == _ORDERED_FOLD_INVALID_VALUE ? :invalid_bounded_value :
code == _ORDERED_FOLD_EMPTY_INPUT ? :empty_bounded_input :
code == _ORDERED_FOLD_INVALID_STEP ? :invalid_step :
:invalid_failure_class
component = publication !== nothing &&
hasproperty(publication.details, :fold_components) &&
Expand Down
33 changes: 30 additions & 3 deletions src/ordered_fold.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ struct _FoldInPlace end
const _ORDERED_FOLD_MAX_EXTENT = Int(typemax(Int32)) - 1
const _ORDERED_FOLD_MAX_COMPONENT_UPDATES = 32
const _ORDERED_FOLD_MAX_TOTAL_UPDATES = 128
const _ORDERED_FOLD_MAX_STEP_BYTES = 1024
# The update-plus-halt envelope remains 1024 bytes; validity and its Int32
# witness add at most eight bytes including alignment to an admitted layout.
const _ORDERED_FOLD_MAX_UPDATE_AND_HALT_BYTES = 1024
const _ORDERED_FOLD_VALIDITY_LAYOUT_BYTES = 8
const _ORDERED_FOLD_MAX_STEP_BYTES =
_ORDERED_FOLD_MAX_UPDATE_AND_HALT_BYTES +
_ORDERED_FOLD_VALIDITY_LAYOUT_BYTES

"""
FoldComponent(target::Field; from=nothing, in_place=false)
Expand Down Expand Up @@ -129,16 +135,24 @@ BoundedWrites(::Type{T}) where {T} =
BoundedWrites{0, T}((), (), Int32(0))

"""
FoldStep(updates; halt=false)
FoldStep(updates; valid=true, witness=Int32(0), halt=false)

Return one kernel-local ordered-fold step. `updates` is a named tuple whose
values are [`BoundedWrites`](@ref). Its names are checked against the declared
accumulator schema during planning; the executor applies components in schema
order rather than caller tuple order. `halt=true` stops only the later canonical
prefix and does not imply rollback.

`valid=false` rejects a transition result before any of its updates reach the
private accumulator scratch. `witness` is an `Int32` diagnostic value reported
with the existing source-item and canonical-position context. A scientifically
valid denial should instead return an ordinary valid step whose updates publish
the denied disposition.
"""
struct FoldStep{Names, U <: NamedTuple}
updates::U
valid::Bool
witness::Int32
halt::Bool
end

Expand All @@ -148,12 +162,14 @@ _fold_writes_tuple(updates::Tuple) =

function FoldStep(
updates::U;
valid::Bool = true,
witness::Int32 = Int32(0),
halt::Bool = false,
) where {U <: NamedTuple}
_fold_writes_tuple(values(updates)) || throw(ArgumentError(
"FoldStep updates must be a named tuple of BoundedWrites values"
))
return FoldStep{keys(updates), U}(updates, halt)
return FoldStep{keys(updates), U}(updates, valid, witness, halt)
end

"""Validate the shared static ABI for a Stage ordered-recurrence transition.
Expand Down Expand Up @@ -192,6 +208,16 @@ function _validate_ordered_fold_step_type(
stage, contract = Symbol(prefix, :_halt_type),
expected = Bool, actual = fieldtype(step_type, :halt),
))
fieldtype(step_type, :valid) === Bool || throw(LocalMathValidationError(
"ordered-fold transition validity must be Bool";
stage, contract = Symbol(prefix, :_valid_type),
expected = Bool, actual = fieldtype(step_type, :valid),
))
fieldtype(step_type, :witness) === Int32 || throw(LocalMathValidationError(
"ordered-fold transition witness must be Int32";
stage, contract = Symbol(prefix, :_witness_type),
expected = Int32, actual = fieldtype(step_type, :witness),
))
update_names = step_type.parameters[1]
Set(update_names) == Set(names) || throw(LocalMathValidationError(
"ordered-fold step updates must name every accumulator component";
Expand Down Expand Up @@ -246,3 +272,4 @@ const _ORDERED_FOLD_DUPLICATE_UPDATE = UInt8(3)
const _ORDERED_FOLD_DUPLICATE_ORDER = UInt8(4)
const _ORDERED_FOLD_INVALID_VALUE = UInt8(5)
const _ORDERED_FOLD_EMPTY_INPUT = UInt8(6)
const _ORDERED_FOLD_INVALID_STEP = UInt8(7)
151 changes: 151 additions & 0 deletions test/fixtures/ordered_fold_step_validation_contracts.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using Test
import KernelAbstractions
import LocalMath

struct OrderedFoldStepValidityDomain end
struct OrderedFoldStepValidityEvaluator end

@inline function (::OrderedFoldStepValidityEvaluator)(
item::Int32, reads, parameters)
imbalance = item == Int32(3) ? getfield(parameters, 1) : Int32(0)
return (event = LocalMath.FoldValue((item, imbalance)),)
end

struct OrderedFoldStepValidityTransition end

@inline function (::OrderedFoldStepValidityTransition)(
state, value, item, reads,
)
event = value[1]
requested = event == Int32(1) ? Int32(3) :
event == Int32(2) ? Int32(8) : Int32(5)
available = state.available[Int32(1)]
realized = requested <= available ? requested : Int32(0)
update_count = realized == Int32(0) ? Int32(0) : Int32(1)
disposition = realized == Int32(0) ? UInt8(0) : UInt8(1)
imbalance = value[2]
available_next = available - realized
received_next = state.received[Int32(1)] + realized + imbalance
balance = available_next + received_next
valid = balance == Int32(10)
witness = balance - Int32(10)
# The invalid result deliberately also proposes an out-of-range write. The
# semantic invalidity must win before structural write validation or apply.
destination = valid ? Int32(1) : Int32(2)
disposition_destination = valid ? event : Int32(4)
return LocalMath.FoldStep((
available = LocalMath.BoundedWrites(
(destination,), (available_next,), update_count),
received = LocalMath.BoundedWrites(
(Int32(1),), (received_next,), update_count),
disposition = LocalMath.BoundedWrites(
(disposition_destination,), (disposition,), Int32(1)),
); valid, witness)
end

function _ordered_fold_step_validity_prepared(array_type)
events = LocalMath.Space(OrderedFoldStepValidityDomain, 3)
resource = LocalMath.Space(OrderedFoldStepValidityDomain, 1)
available_initial = LocalMath.Field(resource, Int32)
available = LocalMath.Field(resource, Int32)
received_initial = LocalMath.Field(resource, Int32)
received = LocalMath.Field(resource, Int32)
disposition_initial = LocalMath.Field(events, UInt8)
disposition = LocalMath.Field(events, UInt8)
imbalance = LocalMath.Parameter(:imbalance, Int32)
state = LocalMath.initialized_state(;
available = LocalMath.FoldComponent(
available; from = available_initial),
received = LocalMath.FoldComponent(
received; from = received_initial),
disposition = LocalMath.FoldComponent(
disposition; from = disposition_initial),
)
publication = LocalMath.Publication((LocalMath.FoldPublication(
LocalMath.PublicationValue(:event)),), LocalMath.OrderedFold(
NTuple{2,Int32}, state, OrderedFoldStepValidityTransition(),
))
stage = LocalMath.Stage(
events, NamedTuple(), (publication,),
LocalMath.Evaluator(OrderedFoldStepValidityEvaluator(), (imbalance,)),
LocalMath.Control(),
LocalMath.SourceOrigin(
@__FILE__, @__LINE__; label = :ordered_fold_step_validity),
)
available_output = array_type(Int32[-41])
received_output = array_type(Int32[-42])
disposition_output = array_type(fill(UInt8(0xff), 3))
work = LocalMath.LocalLaw(stage;
parameters = LocalMath.ParameterSchema(imbalance))
prepared = LocalMath.prepare(
work,
available_initial => array_type(Int32[10]),
available => available_output,
received_initial => array_type(Int32[0]),
received => received_output,
disposition_initial => array_type(zeros(UInt8, 3)),
disposition => disposition_output;
backend = KernelAbstractions.get_backend(available_output),
)
return (;
prepared, available_output, received_output, disposition_output,
)
end

function _ordered_fold_step_validity_execute(prepared, imbalance::Int32)
return try
wait(LocalMath.execute!(prepared; parameters = (; imbalance)))
nothing
catch error
error
end
end

function ordered_fold_step_validation_contracts(array_type)
return @testset "computed ordered-fold step validity is transactional" begin
transition = OrderedFoldStepValidityTransition()
state = (available = Int32[7], received = Int32[3],
disposition = zeros(UInt8, 3))
valid_step = transition(
state, (Int32(3), Int32(0)), Int32(3), ())
invalid_step = transition(
state, (Int32(3), Int32(1)), Int32(3), ())
@test typeof(valid_step) === typeof(invalid_step)

witness = _ordered_fold_step_validity_prepared(array_type)
accepted = _ordered_fold_step_validity_execute(
witness.prepared, Int32(0))
@test accepted === nothing
@test (
available = Array(witness.available_output),
received = Array(witness.received_output),
disposition = Array(witness.disposition_output),
) == (
available = Int32[2],
received = Int32[8],
disposition = UInt8[1, 0, 1],
)

copyto!(witness.available_output, array_type(Int32[-41]))
copyto!(witness.received_output, array_type(Int32[-42]))
copyto!(witness.disposition_output, array_type(fill(UInt8(0xff), 3)))
rejected = _ordered_fold_step_validity_execute(
witness.prepared, Int32(1))
@test rejected isa LocalMath.LocalMathValidationError
@test rejected.contract === :runtime_ordered_fold_validation
@test rejected.actual.failure_class === :invalid_step
@test rejected.actual.component === nothing
@test rejected.actual.source_item == Int32(3)
@test rejected.actual.canonical_position == Int32(3)
@test rejected.actual.witness == Int32(1)
@test (
available = Array(witness.available_output),
received = Array(witness.received_output),
disposition = Array(witness.disposition_output),
) == (
available = Int32[-41],
received = Int32[-42],
disposition = fill(UInt8(0xff), 3),
)
end
end
4 changes: 4 additions & 0 deletions test/metal/ordered_fold_step_validation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include(joinpath(@__DIR__, "..", "fixtures",
"ordered_fold_step_validation_contracts.jl"))

ordered_fold_step_validation_contracts(Metal.MtlArray)
1 change: 1 addition & 0 deletions test/metal/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const LOCALMATH_METAL_WITNESSES = (
"destination_grouping.jl",
"product_values.jl",
"ordered_fold_control.jl",
"ordered_fold_step_validation.jl",
"reduction_control.jl",
"empty_pointwise_domains.jl",
"collect_canonical_order.jl",
Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const LOCALMATH_INCLUDED_TESTS = (
"test_collect_canonical_order.jl",
"test_ordered_fold_stage_model.jl",
"test_ordered_fold_stage_execution.jl",
"test_ordered_fold_step_validation.jl",
"test_ordered_fold_control.jl",
"test_stage_failure_barrier.jl",
"test_stage_collection_binding.jl",
Expand Down
13 changes: 13 additions & 0 deletions test/test_ordered_fold_stage_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ struct OrderedFoldStageTransition end
(Int32(1),), (value,), Int32(1)),))
end

@testset "FoldStep validity metadata preserves bounded update capacity" begin
value = ntuple(_ -> Int64(0), 10)
writes = LMF.BoundedWrites(
ntuple(Int32, 12), ntuple(_ -> value, 12), Int32(12))
updates = (accumulator = writes,)
step = LMF.FoldStep(updates)

@test sizeof(Tuple{typeof(updates),Bool}) == 1024
@test sizeof(step) == 1032
@test LMF._validate_ordered_fold_step_type(
typeof(step), (:accumulator,), (NTuple{10,Int64},)) === typeof(step)
end

@testset "OrderedFold is a terminal typed recurrence Stage law" begin
source = LMF.Space(OrderedFoldStageModelNode, 5)
state_space = LMF.Space(OrderedFoldStageModelNode, 2)
Expand Down
3 changes: 3 additions & 0 deletions test/test_ordered_fold_step_validation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include("fixtures/ordered_fold_step_validation_contracts.jl")

ordered_fold_step_validation_contracts(Array)