loopty 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {loopty-0.1.0 → loopty-0.3.0}/CHANGELOG.md +1192 -0
- loopty-0.3.0/PKG-INFO +1744 -0
- loopty-0.3.0/README.md +1709 -0
- loopty-0.3.0/docs/device-runs/demo_gaps.py +107 -0
- loopty-0.3.0/docs/device-runs/pocl-cpu-demo-gaps.log +15 -0
- loopty-0.3.0/docs/device-runs/pocl-cpu-scatter-atomic.log +43 -0
- loopty-0.3.0/docs/device-runs/scatter_atomic.py +143 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs.md +64 -0
- loopty-0.3.0/docs/lean-checks.md +538 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/loopy-notes.md +331 -40
- {loopty-0.1.0 → loopty-0.3.0}/docs/quickstart.md +146 -73
- loopty-0.3.0/examples/README.md +2782 -0
- loopty-0.3.0/examples/advection.py +929 -0
- loopty-0.3.0/examples/assembly.py +471 -0
- loopty-0.3.0/examples/calls.py +291 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/fusion.py +33 -15
- loopty-0.3.0/examples/hexahedra.py +630 -0
- loopty-0.3.0/examples/l2p.py +549 -0
- loopty-0.3.0/examples/l2p_sumpy.json +39 -0
- loopty-0.3.0/examples/layer_potential.py +601 -0
- loopty-0.3.0/examples/layer_potential_sumpy.json +87 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/p2p.py +9 -13
- loopty-0.3.0/examples/quadrature.py +180 -0
- loopty-0.3.0/examples/storage_layouts.py +175 -0
- loopty-0.3.0/examples/sum_factorization.py +209 -0
- loopty-0.3.0/examples/taylor_reconstruction.py +239 -0
- {loopty-0.1.0 → loopty-0.3.0}/pyproject.toml +8 -2
- loopty-0.3.0/scripts/lean_coverage.py +320 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/__init__.py +15 -1
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/arr.py +11 -0
- loopty-0.3.0/src/loopty/calls.py +1671 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/cli.py +128 -1
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/compose.py +537 -79
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/contract.py +227 -17
- loopty-0.3.0/src/loopty/dataflow.py +1401 -0
- loopty-0.3.0/src/loopty/description.py +632 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/domain.py +30 -2
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/executor.py +174 -20
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/faithful.py +570 -10
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/flow.py +66 -10
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/hypotheses.py +428 -58
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/idx.py +20 -4
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/interpret.py +410 -62
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/kernel.py +513 -24
- loopty-0.3.0/src/loopty/layout.py +1268 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/lower.py +739 -50
- loopty-0.3.0/src/loopty/math.py +331 -0
- loopty-0.3.0/src/loopty/names.py +235 -0
- loopty-0.3.0/src/loopty/obligations.py +3475 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/operations.py +41 -2
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/oracle.py +126 -7
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/promotion.py +100 -19
- loopty-0.3.0/src/loopty/reals.py +457 -0
- loopty-0.3.0/src/loopty/reduction.py +70 -0
- loopty-0.3.0/src/loopty/refusal.py +805 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/schedule.py +3218 -215
- loopty-0.3.0/src/loopty/source.py +252 -0
- loopty-0.3.0/src/loopty/summands.py +721 -0
- loopty-0.3.0/src/loopty/table.py +298 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/term.py +221 -1
- loopty-0.3.0/src/loopty/tested.py +151 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/tolerance.py +8 -3
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/trace.py +1352 -53
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/typing.py +1335 -34
- loopty-0.3.0/src/loopty/unfolding.py +807 -0
- loopty-0.3.0/src/loopty/upstream.py +437 -0
- loopty-0.3.0/src/loopty/window.py +1058 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/conftest.py +16 -0
- loopty-0.3.0/tests/kernels/calls.py +275 -0
- loopty-0.3.0/tests/kernels/dataflow.py +575 -0
- loopty-0.3.0/tests/kernels/program_domains.py +65 -0
- loopty-0.3.0/tests/kernels/program_posts.py +168 -0
- loopty-0.3.0/tests/kernels/reals.py +51 -0
- loopty-0.3.0/tests/kernels/series.py +95 -0
- loopty-0.3.0/tests/kernels/upstream.py +111 -0
- loopty-0.3.0/tests/kernels/windows.py +338 -0
- loopty-0.3.0/tests/lines.py +24 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_adversarial.py +3 -2
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_arithmetic.py +1060 -4
- loopty-0.3.0/tests/test_benchmarks.py +244 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_buildable.py +70 -0
- loopty-0.3.0/tests/test_calls.py +1531 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_cli.py +67 -18
- loopty-0.3.0/tests/test_dataflow.py +949 -0
- loopty-0.3.0/tests/test_demo_shapes.py +235 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_domains.py +15 -0
- loopty-0.3.0/tests/test_examples.py +1896 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_executor.py +28 -2
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_fusion.py +187 -14
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_interpret.py +78 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_isl_reading.py +302 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_kernel.py +10 -8
- loopty-0.3.0/tests/test_layout.py +423 -0
- loopty-0.3.0/tests/test_lean_checks.py +1784 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_lower.py +125 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_lower_traced.py +129 -0
- loopty-0.3.0/tests/test_math.py +640 -0
- loopty-0.3.0/tests/test_names.py +301 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_program.py +18 -11
- loopty-0.3.0/tests/test_program_domains.py +243 -0
- loopty-0.3.0/tests/test_program_posts.py +406 -0
- loopty-0.3.0/tests/test_reals.py +138 -0
- loopty-0.3.0/tests/test_scatter.py +952 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_schedule.py +279 -30
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_smoke.py +1 -1
- loopty-0.3.0/tests/test_source.py +104 -0
- loopty-0.3.0/tests/test_summands.py +145 -0
- loopty-0.3.0/tests/test_table.py +486 -0
- loopty-0.3.0/tests/test_tested.py +191 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_travel.py +67 -5
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_typing.py +76 -3
- loopty-0.3.0/tests/test_upstream.py +264 -0
- loopty-0.3.0/tests/test_window.py +904 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_work_items.py +197 -16
- loopty-0.1.0/PKG-INFO +0 -1001
- loopty-0.1.0/README.md +0 -966
- loopty-0.1.0/examples/README.md +0 -925
- loopty-0.1.0/src/loopty/reduction.py +0 -27
- loopty-0.1.0/tests/test_examples.py +0 -945
- {loopty-0.1.0 → loopty-0.3.0}/.gitignore +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/LICENSE +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/device_demos.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/h200-cuda-commands.log +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/h200-cuda-device-demos.log +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/h200-cuda-reduction-probe.log +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/pocl-epyc-cpu-commands.log +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/pocl-epyc-cpu-device-demos.log +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/pocl-epyc-cpu-reduction-probe.log +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/docs/device-runs/reduction_probe.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/composition.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/pairs.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/reshape_layouts.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/spmv.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/stencil_skew.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/travel.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/examples/wavefront_acoustic.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/scripts/refresh_example_outputs.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/isl_reading.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/plugin.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/src/loopty/py.typed +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/hand_terms.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/kernels/axpy.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/kernels/domains.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/kernels/out_of_bounds.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/kernels/spmv_min.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/kernels/stencil.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/kernels/travel.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/kernels/travel_wrong.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_arr.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_contract_bounds.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_contraction.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_declared_layout.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_executor_c.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_faithful.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_flow.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_idx.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_oracle.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_refresh_script.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_storage.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_term.py +0 -0
- {loopty-0.1.0 → loopty-0.3.0}/tests/test_trace.py +0 -0
|
@@ -6,6 +6,1198 @@ All notable changes to loopty are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-10-15
|
|
10
|
+
|
|
11
|
+
The third release, on lanky 0.3.0. Five demos, each written after a kernel
|
|
12
|
+
of sumpy, pytential, grudge or firedrake: sumpy's L2P (`examples/l2p.py`),
|
|
13
|
+
grudge's weak gradient and divergence on hexahedra (`examples/hexahedra.py`)
|
|
14
|
+
and its advection right-hand side (`examples/advection.py`), pytential's
|
|
15
|
+
direct QBX (`examples/layer_potential.py`) and PyOP2's mass assembly
|
|
16
|
+
(`examples/assembly.py`). Each states its facts, cites the theorems it
|
|
17
|
+
computes, and agrees with the upstream code it mirrors (`@upstream`). isl
|
|
18
|
+
decides each fact, and Lean proves it again from loopty's own description of
|
|
19
|
+
the kernel, with the hints isl found. New in the language and the schedule:
|
|
20
|
+
theorems a kernel cites (`uses=`), a program's postcondition, constant
|
|
21
|
+
tables, `loopty.math`, kernels called inside a loop, a program's arrays over
|
|
22
|
+
a domain, updates and `Schedule.atomic`, window contraction, checked layout
|
|
23
|
+
maps at every size, fusion on chosen outer loops and `Schedule.assume`. The
|
|
24
|
+
demos write their theorems with lanky 0.3.0's families of several indices
|
|
25
|
+
and complex parts, so loopty requires it.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **What fusing and contracting S1's passes buys on a CPU**,
|
|
30
|
+
`benchmarks/hexahedra.py c`: the weak gradient's three programs of
|
|
31
|
+
`examples/hexahedra.py`, the passes one after another, `fused`, and
|
|
32
|
+
`contracted`, on the C target, each checked against numpy and then timed
|
|
33
|
+
in turn on one thread. Every program is built with loopty's flags and the
|
|
34
|
+
same `--cflags`, which the CSV records, and `report` tables the rows as it
|
|
35
|
+
does the device's.
|
|
36
|
+
- **sumpy's GPU schedule in the flagship's ledger** (`examples/l2p.py`):
|
|
37
|
+
`on_a_gpu = fused.retarget("opencl").tag(i="g.0")`, `E2PFromSingleBox`'s
|
|
38
|
+
own, each box a work group, written with the file's names. Its tag's two
|
|
39
|
+
facts are rows of `lanky check` (the fusions' are `fused`'s), the python
|
|
40
|
+
run lists them, and `loopty run` runs it on a device where pyopencl is
|
|
41
|
+
installed and compares it with the native run; on a GPU and on a CPU
|
|
42
|
+
through PoCL the two agreed exactly, up to 4,096 boxes. The same with each
|
|
43
|
+
box's windows, `on_a_device()`, is checked by isl in the python run; Lean
|
|
44
|
+
does not state a cast on windows each work item keeps of its own yet
|
|
45
|
+
(#295).
|
|
46
|
+
- **A schedule for a device, checked where pyopencl is not installed**
|
|
47
|
+
(`loopty.lower.CheckedOpenCLTarget`). A schedule for `"opencl"` is built
|
|
48
|
+
on loopy's plain OpenCL target there, which has the same kernel and
|
|
49
|
+
imports nothing, so a file keeps its device schedules at module level and
|
|
50
|
+
`python`, `lanky check` and `loopty run` read them on any machine.
|
|
51
|
+
Generating or running their code is refused with the reason
|
|
52
|
+
(`ImportError: generating and running OpenCL code needs pyopencl, ...`),
|
|
53
|
+
and `loopty run` says `not run here: running on a device needs pyopencl,
|
|
54
|
+
which is not installed; its facts are checked all the same`, which is no
|
|
55
|
+
failure. `--target opencl` without pyopencl is refused by name, as before.
|
|
56
|
+
- **A window of an array over a domain, of several writers and of a
|
|
57
|
+
recurrence** (`Schedule.contract`, #244, #213). A placement over an array
|
|
58
|
+
over a domain takes the domain's binders, and the window is the box its
|
|
59
|
+
slots span: sumpy's L2P, fused box by box, keeps one box's triangle,
|
|
60
|
+
`contract("R")` finds `lambda i, a, b: (a, b)`, 121 cells for the 66 of a
|
|
61
|
+
triangle of order 10, as plain loopy's temporary of the box. The array may
|
|
62
|
+
be stored by several statements, and read by one of them: the copy of
|
|
63
|
+
the stored rows and the recurrence `R[i, a, b] = -R[i, a - 2, b + 2]`. The
|
|
64
|
+
`definedness` fact asks that every read is of a cell one of them stored
|
|
65
|
+
before it, and the `no-clobber` fact is asked of every pair of them,
|
|
66
|
+
decided by isl and proved by Lean; a window too small, one row of the
|
|
67
|
+
triangle, is refused with the read the recurrence makes, the cell and the
|
|
68
|
+
instance that overwrote its slot. A window of a number of cells is each
|
|
69
|
+
work item's own, as loopy keeps a private temporary, so two of its
|
|
70
|
+
accesses on two work items need no order, and a read on a work item its
|
|
71
|
+
cell's store is not on is refused with the pair; the boxes on `g.0`,
|
|
72
|
+
sumpy's own schedule, each keep their triangle. A window of a size,
|
|
73
|
+
`f_window[n]`, is still one for every work item. A recurrence along a
|
|
74
|
+
loop, `f[j] = f[j - 1] + u[j]`, is a shift register too
|
|
75
|
+
(`registers=True`, the default on OpenCL): its store is the newest
|
|
76
|
+
register and its read the one before. `examples/l2p.py` now
|
|
77
|
+
runs `contracted`, its fused program with the triangle and the table kept
|
|
78
|
+
one box at a time, refuses the row with its read, and checks sumpy's
|
|
79
|
+
schedule on a device; `benchmarks/l2p.py --variant windowed` (and
|
|
80
|
+
`unrolled_windowed`) times it.
|
|
81
|
+
- **A layout at every size** (`loopty.layout`, #263). A layout's body may
|
|
82
|
+
compute with a size its annotations name, so grudge's DOFArray is one
|
|
83
|
+
layout for every order, `n**3 * e + (i + n * j + n**2 * k)` over
|
|
84
|
+
`Fin[ne] x Fin[n]**3`, and so is loopy's `dim_tags` `N3,N0,N1,N2` of it,
|
|
85
|
+
each stride the product of the extents nested inside it. isl cannot state
|
|
86
|
+
`n * j`, so it decides each fact at `n` from 1 to 6 and the fact reads
|
|
87
|
+
`tested`, saying why; Lean proves it for every `n`, given the steps
|
|
88
|
+
`omega` lacks (lanky's products, quotients and multiplying out): the
|
|
89
|
+
products that bound each digit's term, a case split on the first digit
|
|
90
|
+
two cells differ in, and an address's digits by division as the cell it
|
|
91
|
+
stores. Two layouts the same is a polynomial identity Lean multiplies
|
|
92
|
+
out. All in core Lean. A refuted one names the size isl found it at,
|
|
93
|
+
`padded stores (i=0, j=1) and (i=0, j=0) both at 0 (at m=2, n=1)`. A call
|
|
94
|
+
gives the size by name, `dof_array(e, i, j, k, n=4)`.
|
|
95
|
+
`examples/hexahedra.py` declares its three layouts at a symbolic order, so
|
|
96
|
+
its claim that the DOFArray, #418's `dim_tags` and the kernels' row-major
|
|
97
|
+
arrays are one layout holds for every order, where it held at order 3 and
|
|
98
|
+
`as_dim_tags` read `x = x` in Lean.
|
|
99
|
+
- **A device cast proved whole** (#188). A cast that puts a loop on a
|
|
100
|
+
hardware axis claims besides its order that no dependence joins two work
|
|
101
|
+
items, which was stated for isl only, so the cast stayed `decided` where
|
|
102
|
+
Lean is installed. Lean is now asked it too: for each dependence class,
|
|
103
|
+
that the two instances run on one work item of every axis, each one's loop
|
|
104
|
+
counted from where loopy starts it, and for a statement's sum that reads
|
|
105
|
+
what its own instruction writes, that the two never touch one cell where
|
|
106
|
+
they run apart. S1's three device launches, sumpy's tile in S2, Kulkarni's
|
|
107
|
+
split in S3 and the advection right-hand side's launches of every split
|
|
108
|
+
read `proved` by Lean, and the four demo drivers offer their casts to Lean
|
|
109
|
+
where it is installed (`python examples/hexahedra.py`: `along_s: tag(...):
|
|
110
|
+
2 facts, all proved by lean`).
|
|
111
|
+
- **A program's postcondition** (`loopty.unfolding`,
|
|
112
|
+
`loopty.dataflow.program_stores`). A `@program`'s return annotation is a
|
|
113
|
+
claim about what it leaves in its parameters, written in their names and
|
|
114
|
+
the sizes its kernels name, and it is a row of the ledger, keyed as a
|
|
115
|
+
kernel's is:
|
|
116
|
+
- `tested` on the program's native runs, an equation that reads an output
|
|
117
|
+
compared at that output's class, `approx` for reals, as the compiled run
|
|
118
|
+
is compared with the native one: the calls sum in an order of their own,
|
|
119
|
+
and agree with the claim as reals and as floats to rounding. A run that
|
|
120
|
+
leaves it false refutes it, naming the cell and the two values;
|
|
121
|
+
- `decided` by isl where what the calls store decides it, as a kernel's
|
|
122
|
+
postcondition is: each call that stores no cell of an array twice, and
|
|
123
|
+
reads none before its store, has a `single-assignment` fact of the
|
|
124
|
+
program's, and each of its statements is an equation about what the
|
|
125
|
+
program leaves, until a later call writes an array it names;
|
|
126
|
+
- otherwise stated for Lean, a part of Mathlib's for a claim about reals:
|
|
127
|
+
what the calls store unfolded into the claim a pass at a time, each
|
|
128
|
+
theorem the program cites (`@program(uses=[...])`) rewritten where one of
|
|
129
|
+
its sides is, its families at the sections of the arrays they stand
|
|
130
|
+
for, `Fr (3 * x + 0)` and `w (3 * x + 0) e`, then the sums over a literal
|
|
131
|
+
range written out and `ring`. Python finds the steps, and Lean checks
|
|
132
|
+
them; a wrong one is no proof. Proved, the fact rests on the
|
|
133
|
+
`single-assignment` facts and the theorems it used.
|
|
134
|
+
In `examples/hexahedra.py`, `weak_grad` and `weak_div` claim that `grad`
|
|
135
|
+
and `div` are the dense Kronecker operators of the three directions
|
|
136
|
+
applied to the field times its metric terms, citing `sum_factorization`,
|
|
137
|
+
and with Mathlib both rows read `proved lean`: the theorem proved for
|
|
138
|
+
every order now says something about the programs that compute it.
|
|
139
|
+
A claim that names anything but the program's parameters and sizes is
|
|
140
|
+
`assumed`, and says what it names.
|
|
141
|
+
- **A theorem tied to the terms a kernel adds** (`loopty.summands`, #252). A
|
|
142
|
+
cited theorem that concludes `all(c(n) == E for n in Nat if ...)` gives a
|
|
143
|
+
`by-theorem` row where a sum in the kernel's term adds `E` at whole
|
|
144
|
+
numbers `n`, with the theorem's variables bound to the kernel's
|
|
145
|
+
expressions of the sorts it takes them at (a theorem about reals says
|
|
146
|
+
nothing of complex numbers), and an identity the kernel cites, `L == R`,
|
|
147
|
+
rewrites `R` in `E` into the kernel's `L` first; the row names the
|
|
148
|
+
identity's premises that are not the theorem's. `examples/layer_potential.py`'s `single_layer` cites
|
|
149
|
+
`line_taylor` and the new `by_the_conjugate`, `ratio(alpha, beta) == -beta
|
|
150
|
+
/ alpha`, the kernels' ratio by the conjugate, and its ledger reads
|
|
151
|
+
`single_layer adds line_taylor's c(n) for n in 1..5, for every c with away,
|
|
152
|
+
first and ode, read over the complex numbers`: `decided` by `theorem`,
|
|
153
|
+
resting on both theorems, and proved by Lean with Mathlib, the identity
|
|
154
|
+
rewritten and the theorem applied a term at a time. A theorem's annotation
|
|
155
|
+
that calls `loopty.math`'s `conj`, `real`, `exp` and the rest, itself or
|
|
156
|
+
through a helper the kernel calls too, reads them as lanky's functions
|
|
157
|
+
(needs lanky with `real`, `imag` and `conj`), and a kernel's postcondition
|
|
158
|
+
keeps the calls its trace makes (`loopty.math.as_traced`, `as_stated`).
|
|
159
|
+
- **A postcondition's equations of reals at their class** (`loopty.reals`,
|
|
160
|
+
#254). An equation that rounds is compared after each native run within
|
|
161
|
+
the tolerance of the class of what it reads, as the compiled run is with
|
|
162
|
+
the native one, `|a - b| <= 1e-6 (|b| + 1)` for `approx`, and the
|
|
163
|
+
statement says so (`..., to the approx tolerance`); one that rounds
|
|
164
|
+
nothing, one of integers and one of an `exact` class are compared bit for
|
|
165
|
+
bit as before. Where its cells are stored once, by statements whose sums
|
|
166
|
+
run over concrete bounds, Lean proves it with Mathlib over the reals from
|
|
167
|
+
what they store, the tables at the values they hold, resting on the
|
|
168
|
+
`single-assignment` facts and on the `trace-faithful` fact.
|
|
169
|
+
`examples/assembly.py`'s `mass_ffc` now promises the element's mass,
|
|
170
|
+
`|J| (1 + delta_jk) / 24`, in place of a symmetric matrix: `tested` on its
|
|
171
|
+
runs, `proved` with Mathlib, and restated of every cell after its call.
|
|
172
|
+
- **`@upstream(kernel, within=1e-12)`** (#268): the agreement with the code a
|
|
173
|
+
kernel mirrors, compared within the stated fraction of the largest value
|
|
174
|
+
of each output computed in floating point, and the row says so (`... in
|
|
175
|
+
out within 1e-12 of the largest value, on its example inputs`); an `exact`
|
|
176
|
+
output is still compared bit for bit. `examples/layer_potential.py`'s two
|
|
177
|
+
rows state it, where the bound was in a test only.
|
|
178
|
+
- **Claims settled by running them** (`loopty.tested`): `@tested` on a
|
|
179
|
+
function that takes nothing and returns whether its claim held, alone or
|
|
180
|
+
with what it measured, puts the claim in the ledger as a `tested` row,
|
|
181
|
+
decided by `run`, the docstring's first line its statement; `refuted` with
|
|
182
|
+
the reason when it returns false, raises, or returns something that is not
|
|
183
|
+
a verdict. A run is evidence about its inputs, so nothing here reads more
|
|
184
|
+
than `tested`. `examples/layer_potential.py` measures pytential's Green
|
|
185
|
+
identity and the two jump relations this way.
|
|
186
|
+
- **S2 of the demonstration** (`examples/layer_potential.py`): pytential's
|
|
187
|
+
QBX on its own curve. The line-Taylor theorem, for every order, in the same
|
|
188
|
+
file as the kernels that add its closed form, `coefficient(ratio, k)`;
|
|
189
|
+
pytential's single and double layer on the five-armed starfish over complex
|
|
190
|
+
points; sumpy's CPU tile (`sumpy_tile()`), its casts decided, with the
|
|
191
|
+
write sumpy silences (`write_race(write_lpot*)`) the fact `distinct
|
|
192
|
+
instances of S0 write distinct cells of out`, proved by Lean where it is
|
|
193
|
+
installed; each kernel's agreement with what sumpy's own `LayerPotential`
|
|
194
|
+
returned on the example's inputs (recorded at sumpy's commit,
|
|
195
|
+
`layer_potential_sumpy.json`), an `upstream` row; and, as tested rows,
|
|
196
|
+
pytential's identity test case and the jump relations, the errors falling
|
|
197
|
+
faster than the QBX order less 1.6.
|
|
198
|
+
`benchmarks/qbx_layer_potential.py` times the kernel against plain loopy
|
|
199
|
+
and sumpy's own on one OpenCL device, records sumpy's results, and runs
|
|
200
|
+
the kernels on the arguments pytential itself hands sumpy.
|
|
201
|
+
- **S1 of the demonstration** (`examples/hexahedra.py`, #253): grudge
|
|
202
|
+
#418's sum-factorized weak gradient and weak divergence on hexahedra, the
|
|
203
|
+
kernels written for `n` nodes per axis, so that their facts hold at every
|
|
204
|
+
order. The metric product (`metric`, or `contravariant`, which sums the
|
|
205
|
+
metric terms first, three operator applications where grudge makes
|
|
206
|
+
nine), a pass per reference axis (`along_r`, `along_s`) and the last pass
|
|
207
|
+
with the three directions summed (`gradient_along_t`,
|
|
208
|
+
`divergence_along_t`), chained by the programs `weak_grad` and
|
|
209
|
+
`weak_div`. The Kronecker theorem the passes rest on,
|
|
210
|
+
`sum_factorization`, for every `n`, proved by Lean with Mathlib and tested
|
|
211
|
+
without it; the DOFArray's layout, at order 3, the same as #418's
|
|
212
|
+
`dim_tags` `N3,N0,N1,N2` and as the kernels' row-major arrays, each a
|
|
213
|
+
fact. The passes fused as deep as each pair allows, one loop deeper
|
|
214
|
+
refused with its pair and the fusion of fewer loops named, and what
|
|
215
|
+
passes between them kept a row (`w`) and a slab (`v`) at a time; on a
|
|
216
|
+
device, three launches, every node on a work item
|
|
217
|
+
(`passes_on_a_device`). `benchmarks/hexahedra.py` times the variants
|
|
218
|
+
against the same kernels in plain loopy and against grudge on one device,
|
|
219
|
+
and compares them with #418's own `weak_local_grad` and `weak_local_div`
|
|
220
|
+
(`agree`): on a warped mesh at orders 3 to 6 they agree to `3e-15` to
|
|
221
|
+
`5e-15`, natively, compiled, fused and contracted, and as the device
|
|
222
|
+
launches.
|
|
223
|
+
- **The demo's flagship, `examples/l2p.py`**: sumpy's L2P for the 2-D
|
|
224
|
+
Laplace kernel (`E2PFromSingleBox` with the compressed Taylor expansion),
|
|
225
|
+
in one file and one ledger. The claim sumpy's reconstruction rests on,
|
|
226
|
+
for every order (lanky#86), and its form box by box on the triangle `a + b
|
|
227
|
+
<= p`, both proved by Lean with Mathlib; sumpy's storage index as a
|
|
228
|
+
checked layout; `unpack`, whose postcondition, decided from what it
|
|
229
|
+
stores, is sumpy's storage order, `reconstruct`, whose postcondition is
|
|
230
|
+
decided from what it stores and is the theorem's hypotheses, so the
|
|
231
|
+
theorem applies, and `evaluate`, composed by the program `l2p` and fused
|
|
232
|
+
box by box; the interchange of the triangle's loops refused with isl's
|
|
233
|
+
pair, and each box's triangle kept in a window (above). The program agrees with sumpy's own kernel on the file's inputs to
|
|
234
|
+
round-off (`upstream`, below), and with the charges' potential to `4e-13`
|
|
235
|
+
at order 10.
|
|
236
|
+
- **Agreement with the code a kernel mirrors**, `@upstream(kernel)`
|
|
237
|
+
(`loopty.upstream`). The decorated function computes, on the file's
|
|
238
|
+
example inputs, what the code the kernel is written after computes there;
|
|
239
|
+
`lanky check` runs the kernel natively on the same inputs and compares,
|
|
240
|
+
output by output, at the accuracy the kernel's types claim, a dense
|
|
241
|
+
output in its own shape. The row is `tested` by `upstream`, `refuted`
|
|
242
|
+
with the cell and both values (or both shapes), or `assumed` with why
|
|
243
|
+
where the function cannot run: its package is not installed, or its
|
|
244
|
+
recording is not there.
|
|
245
|
+
- **`loopty.math.ipow(x, n)`**, `x` to an integer power multiplied out as
|
|
246
|
+
loopy multiplies one, by squaring, rounded at every product, in both runs:
|
|
247
|
+
natively by the same loop (`loopty.math.integer_power`), compiled as
|
|
248
|
+
loopy's `loopy_pow_<base>_<exponent>`. The two runs agree bit for bit, in
|
|
249
|
+
single precision too. An integer base or a real power is refused, natively
|
|
250
|
+
from the values and traced from the operands' types, and so is the
|
|
251
|
+
smallest power of the type the compiled kernel holds the power in
|
|
252
|
+
(`-2**31`, `-2**63`), whose negation loopy's helper cannot take. `x ** n` of a real `x` stays numpy's power, the C
|
|
253
|
+
library's `pow` in both runs; at the flagship's Taylor sum `ipow` is an
|
|
254
|
+
order of magnitude faster on the C target (`benchmarks/l2p.py`).
|
|
255
|
+
- **`benchmarks/l2p.py`**, which times sumpy's own `E2PFromSingleBox`, the
|
|
256
|
+
same algorithm written directly in plain loopy (loopy #970's L2P
|
|
257
|
+
microkernel with the Laplace step put back, and variants that make
|
|
258
|
+
loopty's choices), and loopty's schedules of `examples/l2p.py`, on one
|
|
259
|
+
tree and one device, and compares their potentials with each other and
|
|
260
|
+
with the direct sum. On the C target plain loopy is built with the flags
|
|
261
|
+
loopty builds its kernels with (`-fwrapv`; `--no-wrapv` for loopy's
|
|
262
|
+
alone), and each record names the flags it was built with. It also
|
|
263
|
+
records sumpy's potential on the example's inputs, for the `upstream`
|
|
264
|
+
row.
|
|
265
|
+
- **The S3 demo: firedrake's PyOP2 assembly loop** (`examples/assembly.py`).
|
|
266
|
+
FFC's P1 mass kernel of firedrake's PyOP2 tests, `mass_ffc`, with its
|
|
267
|
+
15-digit tables as constant tables, is a kernel of its own, called inside
|
|
268
|
+
the loop over the cells as PyOP2 calls it, `mass_ffc(coords[cell[c]],
|
|
269
|
+
Ae[c])`, and the cell's matrix applied to `u` is added into the nodes,
|
|
270
|
+
`y[cell[c, a]] += ...`. The three identities the loop rests on are lanky
|
|
271
|
+
theorems in the same file, proved where Mathlib is installed: the 3-point
|
|
272
|
+
rule integrates every quadratic exactly, a cell's mass matrix is `|J| (1 +
|
|
273
|
+
delta_jk) / 24`, and the scatter keeps the total on every mesh. PyOP2's GPU
|
|
274
|
+
split is refused with the node its two test triangles share and accepted
|
|
275
|
+
once the updates are atomic, and both PyOP2 branches' schedules take the
|
|
276
|
+
assumptions their transforms make (`assume("nc >= 1")`, `assume("nc % 4
|
|
277
|
+
== 0")`). `benchmarks/assembly.py` times loopty's schedules against a
|
|
278
|
+
plain-loopy transcription of PyOP2's wrapper with Kulkarni's and Sun et
|
|
279
|
+
al.'s transforms applied, and with `--nested` against the transcription
|
|
280
|
+
given loopty's nesting of a cell's loops under the cell loop, which is
|
|
281
|
+
what decides where loopy puts the `ilp` loop of Sun et al.'s split
|
|
282
|
+
(interleaving the four cells, or running each whole); and
|
|
283
|
+
`benchmarks/assembly_agreement.py` compares
|
|
284
|
+
the example with firedrake's own assembly and PyOP2's own `par_loop`.
|
|
285
|
+
|
|
286
|
+
- **The advection right-hand side on hexahedra** (`examples/advection.py`),
|
|
287
|
+
the stretch of the S1 demo: grudge #418's `WeakAdvectionOperator` with the
|
|
288
|
+
upwind flux, from grudge's own geometry, as kernels for every order. The
|
|
289
|
+
flux reads the neighbor's value through the face maps, in bounds by type;
|
|
290
|
+
the inverse mass is pushed inward, as grudge #362 rewrites it, and the
|
|
291
|
+
same kernels take the GLL quadrature's tables. Three lanky theorems, proved
|
|
292
|
+
where Mathlib is installed: the two rearrangements, and that the lift onto
|
|
293
|
+
the face nodes is the transpose of the face gather, so it keeps the
|
|
294
|
+
element's total. The kernels fuse on the element, one loop deeper is
|
|
295
|
+
refused with its pair, and the fluxes are kept an element at a time;
|
|
296
|
+
libParanumal's lift onto the face nodes without its barriers is refused
|
|
297
|
+
with the edge node two faces update, and accepted atomic. Five splits on a
|
|
298
|
+
device (libParanumal's volume and surface, the fluxes first, three
|
|
299
|
+
launches, one, one from the metric terms), each launch a checked schedule;
|
|
300
|
+
`benchmarks/advection.py` times them in five shapes beside the same
|
|
301
|
+
launches in plain loopy and grudge's own right-hand side, and compares
|
|
302
|
+
them with grudge's on a warped periodic mesh.
|
|
303
|
+
|
|
304
|
+
- **A kernel's postcondition decided from what its statements store**
|
|
305
|
+
(`loopty.dataflow`, #113). Where no cell of an array a kernel writes is
|
|
306
|
+
stored twice, or read before its store, a `single-assignment` fact says
|
|
307
|
+
so, decided by isl, and what each statement stores is what the kernel
|
|
308
|
+
leaves: its value equation, `off[r + 1] == off[r] + cnt[r]` at every `r`
|
|
309
|
+
of `S1`'s loop. isl decides the postcondition under the equations, each
|
|
310
|
+
taken at the cells the claim reads, so `scan`'s reads `decided isl`,
|
|
311
|
+
resting on the `single-assignment` fact of `off`, and with Lean `proved
|
|
312
|
+
lean`, where it read `tested native`. An equation of reals isl cannot read
|
|
313
|
+
is an atom, one per equation as written, its cells canonical and its sides
|
|
314
|
+
in either order (`discharge(..., atoms=True)`), so `R[a + 2, b] == -R[a, b
|
|
315
|
+
+ 2]` is decided as what the recurrence stores at `(a + 2, b)`; Lean
|
|
316
|
+
states it with Mathlib's reals and proves it by `linarith`. The native
|
|
317
|
+
runs still test the postcondition (`native` in its provenance), and one
|
|
318
|
+
that refutes it refutes it; one the equations do not decide keeps the
|
|
319
|
+
runs' status, with why (`undecided`): a cell stored twice, a read before
|
|
320
|
+
its store (the instance that stores a cell and reads it, however its
|
|
321
|
+
index is spelled, `x[i] = 2 * x[i + 1 - 1] - 5`, reads it first), a write
|
|
322
|
+
under a guard isl cannot state, an update (`y[i] += v`, or a scatter-add
|
|
323
|
+
through a map, `y[cell[c, k]] += v`), whose cells hold what they held on
|
|
324
|
+
entry plus every update: `S0 updates y[cell[c, k]] in place, so a cell of
|
|
325
|
+
y holds what it held on entry plus every update, not what one statement
|
|
326
|
+
stores`.
|
|
327
|
+
- **Theorems a kernel cites**, `@kernel(uses=[on_the_triangle])`. A theorem
|
|
328
|
+
whose hypotheses are conjuncts of the postcondition, up to the names of
|
|
329
|
+
their binders, with a family `R(a)(b)` matching the cell `R[a, b]`, gives
|
|
330
|
+
its conclusion at the kernel's arrays: a `by-theorem` fact, `decided` by
|
|
331
|
+
`theorem` and resting on the theorem and the postcondition, whose
|
|
332
|
+
unbound variables stay universal and whose unmatched hypotheses are its
|
|
333
|
+
own, named in the row. For `examples/taylor_reconstruction.py` it reads
|
|
334
|
+
`by on_the_triangle, for every D with pde and stored: ... R[a, b] ==
|
|
335
|
+
D(a)(b)`. Lean checks the application, the theorem as it was proved
|
|
336
|
+
applied with the arrays loopty matched, so a wrong match is no proof. A
|
|
337
|
+
theorem whose conclusion is a conjunct of the postcondition decides that
|
|
338
|
+
conjunct instead. A match is made only where it says something of what
|
|
339
|
+
the kernel computes: every hypothesis about an array the kernel writes is
|
|
340
|
+
a conjunct of the postcondition (a kernel whose recurrence is off by one
|
|
341
|
+
gets nothing), and a conjunct about reals is matched only where nothing
|
|
342
|
+
in it rounds, copies and sign changes, since the theorem reads it as
|
|
343
|
+
reals. A family into `Nat` or a `Fin` sort stands for an array the kernel
|
|
344
|
+
does not write; the cells it writes take a theorem over `Int`. The
|
|
345
|
+
theorem's variables are put in wherever they are, every sort included: a
|
|
346
|
+
`D: Fn[Fin[n], Real]` nothing binds, with `n` matched to the kernel's `p`,
|
|
347
|
+
ranges over `Fin[p]` (#239), and `m` in `m**2` is the `m` the match bound,
|
|
348
|
+
never a kernel name of the same spelling. A family is applied as `R(a)(b)`,
|
|
349
|
+
`R(a, b)` or `R[a, b]`, as lanky reads them.
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
@kernel(uses=[on_the_triangle])
|
|
353
|
+
def reconstruct(
|
|
354
|
+
c: Arr[Where[a : Fin[2], b : Fin[p + 1], a + b <= p], Real],
|
|
355
|
+
R: Arr[Where[a : Fin[p + 1], b : Fin[p + 1], a + b <= p], Real],
|
|
356
|
+
) -> copies & recurrence: # the two conjuncts, written out in the example
|
|
357
|
+
...
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```console
|
|
361
|
+
$ lanky check examples/taylor_reconstruction.py
|
|
362
|
+
...
|
|
363
|
+
decided under on_the_triangle assumed theorem taylor_reconstruction.py:116 reconstruct by on_the_triangle, for every D with pde and stored: forall a in Fin(...
|
|
364
|
+
...
|
|
365
|
+
```
|
|
366
|
+
- **`lanky check` lists a file's schedules** (#191). The casts of every
|
|
367
|
+
schedule a file holds at module level are rows of its ledger, under the
|
|
368
|
+
kernel or program they schedule, decided by isl as each step was taken
|
|
369
|
+
and proved by Lean where it is installed; two schedules that share their
|
|
370
|
+
first steps share those facts, which are listed once.
|
|
371
|
+
- `examples/taylor_reconstruction.py`: sumpy's compressed Taylor
|
|
372
|
+
reconstruction from the theorem to the kernel to the schedule, in one
|
|
373
|
+
file and one ledger, the seed of the demo's flagship.
|
|
374
|
+
- A matched theorem's family of families applied one index at a time,
|
|
375
|
+
`R(a)(b)`, stands for an array indexed at once, `R[a, b]`, for a program's
|
|
376
|
+
cited theorems too.
|
|
377
|
+
- **Window contraction** (`Schedule.contract`,
|
|
378
|
+
[#132](https://github.com/xywei/loopty/issues/132)): an array a program
|
|
379
|
+
makes, kept in the cells live at once. Fused one step behind, the Burgers
|
|
380
|
+
divergence reads `f[j]` and `f[j - 2]` at step `j`, so
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
fused = Schedule(burgers_rhs).fuse("flux", "divergence", shift=1)
|
|
384
|
+
fused.contract("f") # lambda j: j % 3, double f_window[3]
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
keeps three cells and computes each flux once, where `substitute("f")`
|
|
388
|
+
computes it twice. A placement is a Python function from a cell's indices
|
|
389
|
+
to its slot, quasi-affine, given (`contract("f", lambda j: j % 3)`) or
|
|
390
|
+
found, axis by axis, outermost first, along the order as it stands (one
|
|
391
|
+
row of the sum-factorized passes' `v`, `lambda e, a, b: b`). The step is
|
|
392
|
+
legal when every read is of a cell the producer stored before it (the
|
|
393
|
+
strict `definedness` fact a substitution asks) and no read sees its slot
|
|
394
|
+
overwritten (a new `no-clobber` fact, decided by isl over the schedule's
|
|
395
|
+
order, refuted with the read, the cell it reads and the instance that
|
|
396
|
+
overwrote its slot, `contract('f', lambda j: j % 2) illegal: instance
|
|
397
|
+
divergence.S0[i=14] reads f[13], which flux.S0[j=13] stored in slot 1,
|
|
398
|
+
after flux.S0[j=15] stored f[15] there`). From then on every pair of
|
|
399
|
+
accesses to one slot is a dependence of the schedule, in the order they
|
|
400
|
+
ran, so a later step that breaks the window, or puts its slots on two work
|
|
401
|
+
items, is refused with its pair. Two lowerings: modulo addressing,
|
|
402
|
+
`f_window[j % 3]`, and a shift register (`registers=True`, the default on
|
|
403
|
+
OpenCL) whose every access has a constant index, the window shifted down
|
|
404
|
+
by one at each step of its loop, which a device keeps in registers. The
|
|
405
|
+
zeros the program made the array with are dropped, as a substitution
|
|
406
|
+
drops them. Lean states `no-clobber` from loopty's description: three
|
|
407
|
+
instances through every step's map, the placement with `%` by a number,
|
|
408
|
+
and the time vectors of the order. `examples/fusion.py` and
|
|
409
|
+
`examples/sum_factorization.py` show it; an array its producer reads, or
|
|
410
|
+
that two statements store, and a window per work item are not done (#213,
|
|
411
|
+
#214). A refused window names the fix, `contract('f') finds the smallest
|
|
412
|
+
window that keeps every read, lambda j: j % 3`, and a step that breaks one
|
|
413
|
+
the instance that would now overwrite a slot first. A window and a
|
|
414
|
+
substitution of an array computed from it are refused together, in either
|
|
415
|
+
order (#229): the substitution moves the window's reads. Every window of the
|
|
416
|
+
tests computes the bits the array stored in full does, on C and on an
|
|
417
|
+
OpenCL device, and a shift register inside an outer loop shifts in each of
|
|
418
|
+
its steps.
|
|
419
|
+
- **Checked layout maps** (`loopty.layout`): a layout is a Python function
|
|
420
|
+
from a cell's indices to its address, whose annotations say which cells
|
|
421
|
+
(one `Fin` per axis, or one parameter over a `Where`) and which buffer
|
|
422
|
+
(the return annotation), and `layout(first, second)` is a layout of
|
|
423
|
+
pieces:
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
@layout
|
|
427
|
+
def dof_array(e: Fin[ne], a: Fin[N], b: Fin[N], c: Fin[N]) -> Fin[ne * N**3]:
|
|
428
|
+
return N**3 * e + (a + N * b + N**2 * c)
|
|
429
|
+
|
|
430
|
+
as_dim_tags = dof_array.same_as("N3,N0,N1,N2") # loopy's dim_tags
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`lanky check` asks three facts of each layout, every cell at an address of
|
|
434
|
+
its buffer, no two cells at one address, and a cell at every address, and
|
|
435
|
+
one of a `same_as` claim, every cell stored where the other layout, or
|
|
436
|
+
loopy's `dim_tags`, stores it; isl decides each and Lean proves it, the
|
|
437
|
+
third with isl's inverse of the layout, in pieces with `//` and `%`, as
|
|
438
|
+
its witness. A layout of one piece is called as its function, so a kernel
|
|
439
|
+
reads through it; a call whose address is not the one the facts are about
|
|
440
|
+
(a constant or a helper the function computes with changed since it was
|
|
441
|
+
declared) is refused, with both addresses.
|
|
442
|
+
`examples/storage_layouts.py` checks sumpy's
|
|
443
|
+
compressed-Taylor storage index in two pieces, and grudge #418's
|
|
444
|
+
`N3,N0,N1,N2` against modepy's F-order reshape of the DOFArray; the
|
|
445
|
+
reshape in C order, `N3,N2,N1,N0`, is refuted with a cell, in words:
|
|
446
|
+
`dof_array stores (e=0, a=1, b=0, c=2) at 19, and dim_tags N3,N2,N1,N0 at
|
|
447
|
+
11`; so are two cells at one address and an address with none. A buffer of
|
|
448
|
+
several axes, and an argument that names its layout, are not done (#216).
|
|
449
|
+
- **A kernel called inside another kernel's loop** (`loopty.calls`). A
|
|
450
|
+
kernel body calls a kernel as Python calls a function. A kernel whose
|
|
451
|
+
return annotation is a sort is a rule: it returns its value and writes
|
|
452
|
+
none of its parameters, and a call of it is that value; a kernel that
|
|
453
|
+
writes its parameters is called as a statement and writes through what it
|
|
454
|
+
is given:
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
@kernel
|
|
458
|
+
def dot(a: Arr[Fin[n], Real], b: Arr[Fin[n], Real]) -> Real:
|
|
459
|
+
return reduce_sum(a[k] * b[k] for k in a.dom)
|
|
460
|
+
|
|
461
|
+
@kernel
|
|
462
|
+
def matvec(A: Arr[Fin[m], Fin[n], Real], x: Arr[Fin[n], Real],
|
|
463
|
+
y: Arr[Fin[m], Real]):
|
|
464
|
+
for i in y.dom:
|
|
465
|
+
y[i] = dot(A[i], x)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`A[i]` is the row of `A` and `x[col[r]]` the cells of `x` a row of
|
|
469
|
+
indices picks, as in numpy, so a sparse product is `y[r] = dot(val[r],
|
|
470
|
+
x[col[r]])`; a row of a ragged array, of a table, or a whole array over a
|
|
471
|
+
domain may be passed too. A parameter of a function sort, `f: Fn[Fin[3],
|
|
472
|
+
Real]`, takes a `lambda` or another rule, so that a quadrature rule is
|
|
473
|
+
applied to an integrand, `integrate(lambda q: PHI[q, j] * PHI[q, k])`.
|
|
474
|
+
Traced, the call is inlined: the callee's body runs again in the caller's
|
|
475
|
+
trace against views of the caller's arrays, its loops and sums nested in
|
|
476
|
+
the loops around the call, a binder named like a name of the caller's
|
|
477
|
+
given one of its own (`k_0`), which is loopy's substitution rule with a
|
|
478
|
+
domain of its own (loopy #1044) expanded where it is used, and every fact
|
|
479
|
+
of the caller is about the expansion. The shapes are unified at the call,
|
|
480
|
+
size by size, and a call whose sizes nothing says agree is refused, naming
|
|
481
|
+
the axis. What the callee's contract checks of each call natively, the
|
|
482
|
+
caller owes at every point it calls, as facts isl decides and Lean states
|
|
483
|
+
from loopty's own description of the call: each row passed in bounds
|
|
484
|
+
(`A[i], the row passed to dot as a, is in bounds wherever dot is called`),
|
|
485
|
+
the cells a gather picks in bounds by type, an integral scalar in its sort
|
|
486
|
+
(kind `argument`), two rows of one array apart where the callee writes
|
|
487
|
+
one (kind `distinct-arguments`); and after each call the callee's
|
|
488
|
+
postcondition, restated of what the call passed and resting on the
|
|
489
|
+
callee's own fact (kind `postcondition-in-scope`). An argument is
|
|
490
|
+
evaluated once, when the call is made, as in Python: every cell read for
|
|
491
|
+
the value of a scalar argument, or for the indices of a row (`p[i]` of
|
|
492
|
+
`A[p[i]]`), is owed in bounds at every point of the call, whether the
|
|
493
|
+
callee uses the parameter or not, and a call whose
|
|
494
|
+
callee writes an array an argument reads (`scale(A[i], A[i, 0])`) is
|
|
495
|
+
refused, naming both; so is a function given whose default or captured
|
|
496
|
+
name holds a value read from such an array (`lambda q, saved=x[0]:
|
|
497
|
+
saved`), which natively is the value when the function was made. A
|
|
498
|
+
scalar argument is converted into the sort the callee declares, as its
|
|
499
|
+
contract converts it natively (#232): a double given for `s:
|
|
500
|
+
np.float32` is `np.float32(x[i])` in the caller's term, which the
|
|
501
|
+
interpreter computes as numpy does and the lowering writes as C's cast,
|
|
502
|
+
and a loop variable given for `Int` is a numpy `int64` beside a
|
|
503
|
+
`float32`, while an index or a comparison of it reads as the caller's
|
|
504
|
+
own, `A[i, i]`. What the contract refuses is refused where it is passed:
|
|
505
|
+
a real or a truth value for an integral sort, a number for `Bool`, a
|
|
506
|
+
complex value for a real sort (naming `real(z[i])`), a literal numpy
|
|
507
|
+
does not convert (`300` for `np.int8`), and, when the compiled run is
|
|
508
|
+
asked for, loop arithmetic a narrow integer does not hold where the call
|
|
509
|
+
is made. A callee's positional-only parameters are passed in order
|
|
510
|
+
(#233). A rule may be applied to a function that applies it again, `integrate(lambda q: integrate(lambda p: ...))`; a kernel that
|
|
511
|
+
calls itself, directly or through the functions it is passed, is
|
|
512
|
+
refused. A rule on its own is the kernel that
|
|
513
|
+
writes its value into a one-cell array, `result[0]`, which `loopty run`
|
|
514
|
+
compiles and compares with the native call; one that takes a function is
|
|
515
|
+
compiled where it is called. Natively, a call inside a `when` whose guard
|
|
516
|
+
is false is not made, and a rule's value there is `0`, so the value of a
|
|
517
|
+
rule called inside a `when` block is refused where it is used after the
|
|
518
|
+
block. `examples/calls.py` has `dot` in a matrix-vector
|
|
519
|
+
and a sparse product, and firedrake's P1 quadrature rule applied per
|
|
520
|
+
element. A callee's update, `y[cell[k]] += yl[k]`, is an update of the
|
|
521
|
+
caller, of the class of its sum and owing no distinct writes, as written
|
|
522
|
+
inline: a step that runs two at once is refused with a pair of the
|
|
523
|
+
caller's loop and the callee's read off the example inputs (`S0[c=0, k=2]
|
|
524
|
+
and S0[c=1, k=1] both update y[3]`), and `atomic('y')` on the caller
|
|
525
|
+
lifts it. An integer a callee is given for an `Int`, `np.int64(c)`,
|
|
526
|
+
keeps such an update exact. A window (`Schedule.contract`) is taken over
|
|
527
|
+
the statements of the kernels a program's kernels call, and its
|
|
528
|
+
`no-clobber` fact is about them; a callee reads through a declared
|
|
529
|
+
layout, which is checked against its facts where it is called.
|
|
530
|
+
- **A program's arrays over a domain** (#74). A program's callee may take
|
|
531
|
+
an array over a `Where`, `Sigma` or union domain: the domain's sizes are
|
|
532
|
+
unified across the calls, binder by binder, and two calls given one array
|
|
533
|
+
have to agree on its points, compared as isl sets. A program may make an
|
|
534
|
+
array over a domain, like an argument over one, `Arr.zeros_like(L)`, or
|
|
535
|
+
of a declared shape, whose sizes it reads off its arrays as it does
|
|
536
|
+
natively:
|
|
537
|
+
|
|
538
|
+
```python
|
|
539
|
+
@program
|
|
540
|
+
def pair_sums(x, e):
|
|
541
|
+
f = Arr.zeros(TRIANGLE, n=x.dom.size)
|
|
542
|
+
fill(x, f)
|
|
543
|
+
rowsum(f, e)
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
It is a temporary of the lowered kernel, stored boxed and zeroed over its
|
|
547
|
+
points where the program makes it, its edges have definedness facts as a
|
|
548
|
+
dense temporary's do, and a schedule fuses through it; `Schedule.pack`
|
|
549
|
+
stores a program's argument over a domain packed. A declared dense shape
|
|
550
|
+
is `Arr.zeros((x.dom.size, 3))`, and a run whose sizes make one of its
|
|
551
|
+
lengths negative is refused compiled, as numpy refuses it natively.
|
|
552
|
+
`Schedule.contract` may keep a dense one in a window; one over a domain is
|
|
553
|
+
refused there, naming the fix, keep it whole or `substitute` it
|
|
554
|
+
([#244](https://github.com/xywei/loopty/issues/244)).
|
|
555
|
+
|
|
556
|
+
- **Constant tables** (`loopty.table`): quadrature weights, a basis
|
|
557
|
+
tabulated at the quadrature points, a differentiation matrix, declared
|
|
558
|
+
next to the kernel as the numpy array they are, `W = table([1 / 6, 1 / 6,
|
|
559
|
+
1 / 6])`, and read by the body as an array is, `W[q]`, `W.dom`. Natively a
|
|
560
|
+
table is a read-only copy of its values. Traced, it is recorded in the
|
|
561
|
+
term (`Term.tables`) under the name the body reads it by, and each read is
|
|
562
|
+
an access whose in-bounds fact isl decides over the table's shape. Lowered,
|
|
563
|
+
it is a read-only loopy temporary with the values as its initializer, which
|
|
564
|
+
loopy writes once with every digit, `static double W[3] = {
|
|
565
|
+
0.16666666666666666, ... };` on the C target and `__constant` on OpenCL, so
|
|
566
|
+
both runs read the same numbers; the interpreter reads them too. Complex,
|
|
567
|
+
single precision and integer tables are read in their own dtype. A table
|
|
568
|
+
read in a helper of the body is the kernel's, named as it is declared, a
|
|
569
|
+
program holds one table for every call that reads it, and a table named
|
|
570
|
+
like a parameter or a size of the kernel is named apart, `y_table`.
|
|
571
|
+
Writing one is a `TypeError` in both runs. Natively a table is read as an
|
|
572
|
+
array is: an index is never counted from the end, and a read out of range
|
|
573
|
+
under a false `when` answers zero. A table holds finite numbers (#193), in
|
|
574
|
+
a type the compiled kernel has (not `float16` or `longdouble`),
|
|
575
|
+
is no argument (an array of its values is,
|
|
576
|
+
`Arr.from_numpy(W.values.copy())`), and is read in a statement's values,
|
|
577
|
+
not in a loop's bound (#194); each of these is refused with the fix named.
|
|
578
|
+
A compiled run refuses a read-only array for a parameter it writes, such
|
|
579
|
+
as a table's `W.values`, which it wrote into before failing. On OpenCL a
|
|
580
|
+
complex table is an array of the kernel's own, since pyopencl writes its
|
|
581
|
+
values as calls, which a `__constant` array cannot be initialized by.
|
|
582
|
+
- **`loopty.math`**: `sqrt`, `exp`, `log`, `sin`, `cos`, `real`, `imag` and
|
|
583
|
+
`conj` for kernel bodies. Each is numpy's natively and a call the
|
|
584
|
+
trace records, which the interpreter evaluates with the same numpy function
|
|
585
|
+
and the lowering writes as the target's library function, `csqrt` and
|
|
586
|
+
`creal` of a complex number on the C target and pyopencl's `cdouble_sqrt`
|
|
587
|
+
and `cdouble_real` on OpenCL, in the type numpy computes it in: `sqrt` of
|
|
588
|
+
an integer is a double (`sqrt((double) (k[i]))`), where loopy refused it on
|
|
589
|
+
the C target and took it for a `float` on OpenCL, and `real`, `imag` and
|
|
590
|
+
`conj` of a number that is not complex are the number, `0` and the number,
|
|
591
|
+
where loopy wrote `creal` of a double. A function numpy computes in half
|
|
592
|
+
precision, `sqrt` of an `int8`, is a `TraceError` naming `sqrt(1.0 *
|
|
593
|
+
k[i])`. pytential's line-Taylor single layer over complex points,
|
|
594
|
+
`real(log(c[t] - y[s]) - ...)`, runs, checks and compiles as any kernel
|
|
595
|
+
(`examples/layer_potential.py`). A postcondition may call them too, `->
|
|
596
|
+
all(y[i] == sqrt(x[i]) for i in Fin[n] if x[i] >= 0)`, and is tested after
|
|
597
|
+
each native run with numpy's, where it was left `assumed`, not evaluated.
|
|
598
|
+
`examples/p2p.py` calls `loopty.math.sqrt` where it built the call by hand.
|
|
599
|
+
A value of one compares and combines as any traced value does,
|
|
600
|
+
`when(sqrt(x[i]) > 1.5)`. numpy's own functions, Python's `math` and
|
|
601
|
+
`x[i].real` take numbers: a body that calls them on a traced value is
|
|
602
|
+
refused at its line, with the `loopty.math` function to write (#195),
|
|
603
|
+
where it failed far from the body (numpy's "loop of ufunc does not support
|
|
604
|
+
argument 0 of type Subscript", pymbolic's `UnknownVariableError`).
|
|
605
|
+
- **A sum's loop ordered outside a loop of its statement**
|
|
606
|
+
(`Schedule.prioritize("s_outer", "t_inner")`). sumpy's CPU tile of
|
|
607
|
+
a layer potential splits the targets by 16 onto a group and its work
|
|
608
|
+
items, splits the sources by 256, and runs the sources' outer loop
|
|
609
|
+
outside the work items' loop; `prioritize` and `interchange` refused the
|
|
610
|
+
sum's loop as no loop of the kernel. A loop of a sum named before loops
|
|
611
|
+
of its statement now runs outside them, each sum still adding its terms
|
|
612
|
+
in its own order, so nothing is reassociated; a loop the sum runs outside
|
|
613
|
+
of orders none of the instances in it, so a dependence between two of
|
|
614
|
+
them is refused with its pair, as one across an `ilp` loop is. loopy runs
|
|
615
|
+
the order only where it keeps an accumulator per instance, on a hardware
|
|
616
|
+
axis or an `ilp` loop (`acc[16]` on the C target); on a loop that runs in
|
|
617
|
+
sequence it keeps one and drops the order, and the schedule's `buildable`
|
|
618
|
+
fact is refuted with that reason and the tag that fixes it. Two loops of
|
|
619
|
+
one sum are never reordered, and an inner one never runs outside a loop
|
|
620
|
+
its outer one does not.
|
|
621
|
+
- **Fusion on chosen outer loops** (`Schedule.fuse(..., loops=1)`). A
|
|
622
|
+
fusion took every loop both sides have, so two passes over elements, the
|
|
623
|
+
second of which reads every cell of the element the first wrote (a
|
|
624
|
+
sum-factorized operator, one reference axis per pass), could not be fused
|
|
625
|
+
at all: fused on the element and the row, the second pass reads a cell of
|
|
626
|
+
the element before the first stores it. `loops` fuses only that many of
|
|
627
|
+
the outer loops and leaves the loops inside them one after the other, the
|
|
628
|
+
producer's first, checked as every fusion is. A refused fusion now names
|
|
629
|
+
each fusion it checked that is accepted: the least shift at the same
|
|
630
|
+
depth, as before, and the deepest fusion of fewer loops, `fuse('along',
|
|
631
|
+
'along@2', loops=1)`.
|
|
632
|
+
|
|
633
|
+
- **isl decides, Lean checks** (`loopty.description`, `loopty.obligations`).
|
|
634
|
+
Every fact isl decides about a kernel, a program or a schedule's step is
|
|
635
|
+
also stated as lanky terms for lanky's Lean oracle, which proves it with
|
|
636
|
+
`omega`; where Lean is installed such a fact reads `proved` by `lean`, and
|
|
637
|
+
where it is not, nothing changes. The statements come from loopty's own
|
|
638
|
+
description of the kernel and never from a set isl computed: every set
|
|
639
|
+
loopty builds keeps the text it was written from, on the set itself
|
|
640
|
+
(`description_of`), with each call's renaming, and every schedule step a
|
|
641
|
+
record of its maps as written (`Schedule._records`, `loopty.schedule.Step`).
|
|
642
|
+
In bounds, distinct writes, element sorts, the source order, a cast's
|
|
643
|
+
bijectivity and its legality (one part per dependence class), and what a
|
|
644
|
+
program's call may assume (requirements and flat accesses under the
|
|
645
|
+
hypotheses isl used) are stated. isl's work is used as hints Lean checks:
|
|
646
|
+
its functional form of a step's map as the witness that the step is total,
|
|
647
|
+
its `lexmax` pieces as the witness of a definedness, the instances it took
|
|
648
|
+
of a universal hypothesis, and its counterexample to a refuted fact, which
|
|
649
|
+
Lean proves false. A refuted fact is offered to Lean only as that
|
|
650
|
+
counterexample, and one with none Lean can check (a refused bijection, say)
|
|
651
|
+
is not offered at all, so that a proof never reads `proved` where isl
|
|
652
|
+
refused. A map given to `Schedule.affine` as isl writes it (`2 t + i`,
|
|
653
|
+
`floor(i / 4)`) is read back too. Facts loopty states for isl alone (a
|
|
654
|
+
rewritten layout, the casts after a substitution, a cast with a hardware
|
|
655
|
+
axis) say so. `docs/lean-checks.md` explains it, and
|
|
656
|
+
`scripts/lean_coverage.py` reports what Lean proves of every example and
|
|
657
|
+
test kernel.
|
|
658
|
+
- `loopty run` asks Lean about each fact isl decided that it reports (the
|
|
659
|
+
casts, a layout's facts), names what Lean did not prove under the table
|
|
660
|
+
(with `--verbose`, also what Lean was not asked about, and why), and prints
|
|
661
|
+
the Lean with `--show-lean [TEXT]`; `loopty check --show-lean` passes it to
|
|
662
|
+
`lanky check`.
|
|
663
|
+
- **Definedness inside one kernel.** An array a kernel reads only at cells
|
|
664
|
+
an earlier instance of its own wrote, as `scan` reads `off[r]`, gets a
|
|
665
|
+
`definedness` fact where isl shows it, stated for Lean as a ∀∃ with isl's
|
|
666
|
+
last writer as the witness. A kernel that reads what an array held when it
|
|
667
|
+
was called gets none, and a write under a guard isl cannot state
|
|
668
|
+
(`with when(x[i] > 0)`) counts for nothing, since its domain is wider than
|
|
669
|
+
where it stores. A program's definedness of an edge is stated the same
|
|
670
|
+
way.
|
|
671
|
+
- **Mixed subscripts.** An access whose indirect axes are indexed by a value
|
|
672
|
+
of their own sort and whose other axes are affine, `X[cell[c, k], d]`, is in
|
|
673
|
+
bounds by type on the first and decided by isl on the rest, where it was
|
|
674
|
+
`assumed`; Lean states the whole access with the type as a hypothesis.
|
|
675
|
+
- **`Schedule.assume(condition)`**: a condition of the sizes, `"n % 4 == 0"`,
|
|
676
|
+
taken as given, as loopy's `assume`. loopy is told, every later cast is
|
|
677
|
+
decided and stated where it holds and rests on an `assumption` fact, and a
|
|
678
|
+
call whose sizes break it is refused before anything is compiled.
|
|
679
|
+
- **Updates, a scatter-add: `y[cell[c, k]] += yl[c, k]`.** A statement
|
|
680
|
+
that adds to the cell it writes a value that reads nothing else of the
|
|
681
|
+
array is an update (`Stmt.exactness`). Its sum has the class of what it
|
|
682
|
+
adds, as a reduction's has: `approx` for `Real`, `exact` for integers. Two
|
|
683
|
+
updates of one cell give the same sum in either order, so they are no
|
|
684
|
+
dependence of each other (`loopty.flow.dependences`), and a schedule may
|
|
685
|
+
reorder them, which it records as a reassociation, as `realize` does. An
|
|
686
|
+
update whose instances can add to one cell owes the class of its sum, `the
|
|
687
|
+
accumulation into y[cell[c, k]] over c, k is approx`, instead of distinct
|
|
688
|
+
writes, which were refuted before: a correct assembly loop or row sum
|
|
689
|
+
written with `+=` now checks clean. An update of a cell of its own at
|
|
690
|
+
every instance, `y[i] += a * x[i]`, still owes distinct writes.
|
|
691
|
+
- **`Schedule.atomic("y")`.** Two updates of one cell that a step runs at
|
|
692
|
+
the same time, on two work items or under `ilp` or `vec`, can lose one of
|
|
693
|
+
the two values. Such a step is refused with the pair and the fix:
|
|
694
|
+
`tag(c_outer='g.0', c_inner='l.0') illegal: S1[c=0, k=2] and S1[c=1, k=1]
|
|
695
|
+
both update y[3] on example_inputs() (nc=2, nv=4), where cell[0, 2] = 3
|
|
696
|
+
and cell[1, 1] = 3, ...; make the updates atomic, with atomic('y') before
|
|
697
|
+
this step`. After `atomic("y")` the step is accepted, it records `atomic
|
|
698
|
+
updates of y run at the same time after tag(...)`, and `y` is compared at
|
|
699
|
+
`reassoc`. On OpenCL each update is loopy's `AtomicUpdate`, a
|
|
700
|
+
compare-and-swap loop, and every other access to `y` is atomic too, as
|
|
701
|
+
loopy requires. loopy has atomic updates of `int32`, `int64`, `float32`
|
|
702
|
+
and `float64` only; any other dtype is a `buildable` fact refuted. On the
|
|
703
|
+
C target, which runs in one thread, the code is unchanged. `atomic`
|
|
704
|
+
refuses an `exact` accumulation, as `realize` refuses a tree of one
|
|
705
|
+
(#200), and an array nothing updates, naming what each statement that
|
|
706
|
+
writes it does instead. `examples/assembly.py` is PyOP2's P1 mass action:
|
|
707
|
+
its GPU split refused with the node PyOP2's two triangles share, accepted
|
|
708
|
+
with atomics, and run on the PoCL CPU device (`docs/device-runs.md`).
|
|
709
|
+
An update may read a window (`Schedule.contract`), `y[node[i]] += f[i +
|
|
710
|
+
1] - f[i - 1]`, and `atomic` does not lift the order of the window's
|
|
711
|
+
slots; a window of an array that is updated is refused, since the
|
|
712
|
+
updates add to the zeros a window drops, with the fix: `contract('f'):
|
|
713
|
+
added_flux.S0 updates f[j] in place, ...; store each cell once, f[j] =
|
|
714
|
+
... (a reduce_sum where the updates run over a loop), or keep f whole`.
|
|
715
|
+
- **Refusals name a pair from the data.** isl cannot follow a cell read
|
|
716
|
+
from an array, `y[cell[c, k]]`: it takes it to be any cell, so the pair it
|
|
717
|
+
picks need not share one. A refused cast through such a cell now names two
|
|
718
|
+
instances that touch one cell on an input, with what the subscripts read
|
|
719
|
+
there. The inputs are tried in turn: the schedule's own example inputs,
|
|
720
|
+
the module's `example_inputs()`, then drawn ones. Where none has such a
|
|
721
|
+
pair, the message says the pair is isl's, naming the subscript isl cannot
|
|
722
|
+
follow; so it does for a read a substitution moves (`substitute('f')
|
|
723
|
+
reads y where f was read, which no run of an input shows`), which no
|
|
724
|
+
input's run makes. `loopty.interpret.interpret(..., log=...)` records the
|
|
725
|
+
cells each instance touches.
|
|
726
|
+
|
|
727
|
+
### Changed
|
|
728
|
+
|
|
729
|
+
- **The README opens with a front page**: what loopty is to someone who
|
|
730
|
+
writes loopy kernels, the flagship `examples/l2p.py` in its three layers
|
|
731
|
+
with its refusals and six rows of its ledger with Lean and Mathlib, the
|
|
732
|
+
five demos in a table (what each is written after, what its ledger has,
|
|
733
|
+
how it compares), how to run them, and where the limits are.
|
|
734
|
+
The reference below it is as it was, the spmv walk-through under its own
|
|
735
|
+
heading and the paragraphs the front page now says shortened. Run it and
|
|
736
|
+
Install install the 0.3.0 releases from PyPI and clone the `v0.3.0` tag
|
|
737
|
+
for the examples.
|
|
738
|
+
- **Requires lanky 0.3.0** (`lanky>=0.3.0`, was `>=0.2.0`). Its Lean oracle
|
|
739
|
+
proves a plugin's parts with the solver's hints, which is how loopty
|
|
740
|
+
states each fact isl decides, and its families take several indices,
|
|
741
|
+
`D[a, b]`, and complex numbers apart, which the demos' theorems are written
|
|
742
|
+
with. The tests that skipped on lanky 0.2.0 (the demos `l2p`, `hexahedra`,
|
|
743
|
+
`advection` and `layer_potential`, the benchmark driver's test, the
|
|
744
|
+
statements for Lean, rewrites by an identity) no longer skip, so CI's job
|
|
745
|
+
against the released lanky runs them. `[tool.uv.sources]` still resolves
|
|
746
|
+
lanky from `../lanky` in a checkout, and the built distributions ask for it
|
|
747
|
+
from PyPI.
|
|
748
|
+
- **The interpreter finds a sum's points once, not at every instance**
|
|
749
|
+
(lanky #125). The faithful check runs each kernel's term through
|
|
750
|
+
`loopty.interpret`, and a reduction asked isl for its points at every
|
|
751
|
+
statement instance: it fixed the loop variables, the domain's parameters
|
|
752
|
+
and enumerated. Now a reduction's domain is enumerated once per run at
|
|
753
|
+
the values of its parameters, its points kept by the loop variables each
|
|
754
|
+
instance fixes, and where it is the loops' points times the sum's (a sum
|
|
755
|
+
over `Fin[n]` in a loop nest), as its two factors. Each instance reads its
|
|
756
|
+
own points, in the same order, and the work limit is checked on them by
|
|
757
|
+
their bounding box as before. The rule for a node is found once per type.
|
|
758
|
+
`lanky check examples/hexahedra.py` spends 7 s in the interpreter where it
|
|
759
|
+
spent 31, and takes 12 s of CPU where it took 38 without Lean; every
|
|
760
|
+
example's ledger, JSON included, is what it was.
|
|
761
|
+
- **`loopty run` says what a row lacks to be proved**, with lanky's
|
|
762
|
+
`WITHOUT LEAN` and `WITHOUT MATHLIB` lines under its table, and sizes the
|
|
763
|
+
table's statement column to the terminal, as `lanky check` does, where the
|
|
764
|
+
lanky it runs with has them. The transcripts show each statement whole,
|
|
765
|
+
wrapped under its row.
|
|
766
|
+
- A refusal names each instance of its pair by the statement as the source
|
|
767
|
+
writes it, the line it is on, and its coordinates by the names the source
|
|
768
|
+
gives its loops (#260). The flagship's interchange reads `R[i, a, b] =
|
|
769
|
+
C[i, a, b] (l2p.py:276) at i=0, a=1, b=9 writes R[0, 1, 9], read by R[i,
|
|
770
|
+
a, b] = -R[i, a - 2, b + 2] (l2p.py:278) at i=0, a=3, b=7, scheduled
|
|
771
|
+
earlier`, where it read `instance S0[i=0, a=1, b=9] writes R[0, 1, 9]
|
|
772
|
+
read by S1[i=0, a=3, b=7] scheduled earlier`; S3's race, `y[cell[c, a]]
|
|
773
|
+
+= reduce_sum(...) (assembly.py:269) at c=0, a=2 and at c=1, a=1 both
|
|
774
|
+
update y[3]`. A long statement keeps what it writes into and shortens the
|
|
775
|
+
calls on its right, `reduce_sum(...)`; an instance of the statement just
|
|
776
|
+
named is `the same statement at t=1, i=7`. The same holds for every
|
|
777
|
+
refusal that names a pair (of `tile`, `interchange`, `skew`, `affine`,
|
|
778
|
+
`fuse`, `tag`, `contract`, `substitute`), and for a disjoint-writes fact
|
|
779
|
+
refuted on an input, and the fix each names is kept. A pair on two work
|
|
780
|
+
items says where each access runs by what it does, `the write is in a
|
|
781
|
+
sum, on every work item of l.0`, where it named the statement a second
|
|
782
|
+
time. A fact's `witness` keeps the ids, `S0[t=0, i=8]`, and so does a
|
|
783
|
+
term written by hand, which has no source.
|
|
784
|
+
- A schedule's facts are at the line of the step that made them, the
|
|
785
|
+
`.split(...)` or the `.assume(...)` of a chain, and its run at the line
|
|
786
|
+
the schedule is finished, its last step (#259). They were all at the
|
|
787
|
+
kernel's first statement, which for S3's `mass_action`, whose first
|
|
788
|
+
statement is the one of `mass_ffc` it calls, was a line in `mass_ffc`.
|
|
789
|
+
The run of a kernel the file does not schedule is at the kernel's
|
|
790
|
+
definition.
|
|
791
|
+
- A sum over a `Fin` of a number, which the trace adds up term by term, has
|
|
792
|
+
the facts of its terms' accesses stated once, over its binder, as the
|
|
793
|
+
source writes the access (#181, #223): S1's `contravariant` reads
|
|
794
|
+
`G[3*x + d, e, k, j, i] is in bounds for every x in Fin(3) and every
|
|
795
|
+
instance of S0`, where it listed `G[0 + d, ...]`, `G[3 + d, ...]` and `G[6 +
|
|
796
|
+
d, ...]`, and the three sums over `c` of `gradient_along_t` are one
|
|
797
|
+
exactness row, `for every d in Fin(3)`. The term, and the code, still
|
|
798
|
+
add the terms one by one. S1's ledger is 80 facts where it was 96, with
|
|
799
|
+
the same verdicts.
|
|
800
|
+
- **A cast's fact id leaves out the target.** What a step means is the same
|
|
801
|
+
on every target, so the id of a cast's facts is the kernel's definition and
|
|
802
|
+
the steps (`cast:spmv.spmv@102:split('j', 2, ...):bijective`), and a
|
|
803
|
+
schedule retargeted (`fused.retarget("opencl").tag(i="g.0")`) shares the
|
|
804
|
+
facts of the steps it replays with the one it came from: a ledger lists
|
|
805
|
+
each fusion once. A `buildable` fact and a run's agreement keep the
|
|
806
|
+
target (`agreement:...:[c].split(...)`).
|
|
807
|
+
- **A substitution through an array computed from inputs alone.**
|
|
808
|
+
`Schedule.substitute` computes an array where it is read from what its
|
|
809
|
+
producer read, and refused one whose producer reads an array an earlier
|
|
810
|
+
substitution computes where it is read, since the dependences of that
|
|
811
|
+
array's own reads were carried to a statement that no longer runs. Where
|
|
812
|
+
that array is computed from arrays nothing in the program writes (its
|
|
813
|
+
inputs), there are none, and the chain is substituted: grudge's `v.n`
|
|
814
|
+
from the normals, read by the upwind flux, read by the lift
|
|
815
|
+
(`examples/advection.py`). A chain through an array something writes is
|
|
816
|
+
refused as before.
|
|
817
|
+
|
|
818
|
+
- `contract` of a program's array over a domain names `substitute` as the
|
|
819
|
+
other fix only where one statement stores the array, which is what a
|
|
820
|
+
substitution computes again; the flagship's triangles, which two
|
|
821
|
+
statements store, are refused with `keep it whole`.
|
|
822
|
+
|
|
823
|
+
- A kernel's postcondition that its statements' stores decide reads
|
|
824
|
+
`decided isl` where it read `tested native`, after a new `single-assignment`
|
|
825
|
+
row, and the restatements of it in programs are worth `decided`; the
|
|
826
|
+
ledger of a file with schedules at module level lists their casts, a
|
|
827
|
+
window's `no-clobber` and an atomic step's reassociation among them. The
|
|
828
|
+
`definedness` fact a substitution or a window asks of a schedule leads
|
|
829
|
+
with its step, `for substitute('f'), every cell of f that burgers_rhs
|
|
830
|
+
reads, flux.S0 has stored by the time it is read`, so that the two rows of
|
|
831
|
+
`examples/fusion.py`, one per step after one fusion, read apart. The
|
|
832
|
+
transcripts in the documentation are regenerated.
|
|
833
|
+
- `loopty.hypotheses.discharge` names a universal hypothesis's binders apart
|
|
834
|
+
from the claim's own names before taking it at the claim's cells: an
|
|
835
|
+
instance at the claim's `r` of a hypothesis over `r` was left out, which
|
|
836
|
+
lost a verdict.
|
|
837
|
+
- Where Lean is installed, a rewritten layout's facts are proved
|
|
838
|
+
([#186](https://github.com/xywei/loopty/issues/186)): one decided by the
|
|
839
|
+
row starts is stated as the induction step, each write of the offsets
|
|
840
|
+
storing the value its cell holds where they start at 0 and step by the
|
|
841
|
+
counts, as the contract checks on entry; one decided by induction over the
|
|
842
|
+
run, as each write's question under the order before it. And a
|
|
843
|
+
substitution's `definedness` fact and the casts after a substitution are
|
|
844
|
+
proved ([#187](https://github.com/xywei/loopty/issues/187)): the reads a
|
|
845
|
+
substitution computes again are stated as reads of what the producer read,
|
|
846
|
+
at the instance that stored the cell, ordered against every write of it,
|
|
847
|
+
and the statements it took out are in no class. So every fact isl decides
|
|
848
|
+
about the examples is now stated for Lean, and the coverage report in
|
|
849
|
+
`docs/lean-checks.md` is rerun. A claim decided under hypotheses is given
|
|
850
|
+
up to 32 of isl's instances of them, where it was given 16.
|
|
851
|
+
- `substitute` refuses an array kept in a window, and `contract` one
|
|
852
|
+
computed where it is read.
|
|
853
|
+
- The coverage report in `docs/lean-checks.md` covers the demos' examples
|
|
854
|
+
(`l2p.py`, the flagship, `assembly.py`, `hexahedra.py`,
|
|
855
|
+
`layer_potential.py`, `advection.py`, `quadrature.py`,
|
|
856
|
+
`sum_factorization.py`), `storage_layouts.py`, `taylor_reconstruction.py`,
|
|
857
|
+
the calls in loops (`examples/calls.py`, `tests/kernels/calls.py`,
|
|
858
|
+
`tests/kernels/program_domains.py`), the upstream test kernels
|
|
859
|
+
(`tests/kernels/upstream.py`), the test kernels of programs'
|
|
860
|
+
postconditions, of reals at their class and of series
|
|
861
|
+
(`tests/kernels/program_posts.py`, `reals.py`, `series.py`), and the
|
|
862
|
+
windows, updates, layouts, postconditions decided from what kernels store
|
|
863
|
+
and cited theorems above: of 1584 facts isl decides in 34 files, Lean
|
|
864
|
+
proves 1521 and confirms the counterexample of 37 of the 38 refuted ones;
|
|
865
|
+
the other, two updates of one cell at the same time in `advection.py`, is
|
|
866
|
+
stated for isl only. The 25 it leaves decided are claims about reals,
|
|
867
|
+
which core Lean has no words for; with Mathlib it proves them too, 1546 in
|
|
868
|
+
all, and none stays decided. `examples/layer_potential.py` has 32, all
|
|
869
|
+
proved with Mathlib. `examples/l2p.py` has 48: 46 proved with Mathlib, and
|
|
870
|
+
its refused interchange and window of one row, whose negations Lean
|
|
871
|
+
proves.
|
|
872
|
+
- A ledger gains a `definedness` row for each array a kernel reads only where
|
|
873
|
+
it wrote it before (`scan`'s `off`, `p2p`'s `term`, `pairs`' `f`), and an
|
|
874
|
+
access with mixed subscripts is decided where it was assumed; the
|
|
875
|
+
transcripts in the documentation are regenerated.
|
|
876
|
+
- The questions isl answers (`Empty`, `Subset`, `Bijective`, `Monotone`)
|
|
877
|
+
carry a `lean` field, how the fact is stated for Lean, and a schedule's
|
|
878
|
+
cast facts have a `Cast` as their term where they had none (a
|
|
879
|
+
substitution's definedness fact an `IslOnly`, which says why Lean is not
|
|
880
|
+
asked).
|
|
881
|
+
- The tests run with lanky's Lean oracle off, as CI runs them, but the ones
|
|
882
|
+
marked `lean` (`tests/test_lean_checks.py`), so that the suite passes where
|
|
883
|
+
Lean is installed and proves what the other tests read as `decided isl`.
|
|
884
|
+
Those hold the printer to its safeguards: non-vacuity of every part of
|
|
885
|
+
every example and test kernel, arrays given as tables; a mutation matrix of
|
|
886
|
+
the stencil's subscripts, guard and schedules, where Lean has to fail on
|
|
887
|
+
exactly the dependence classes isl finds reversed; and a differential of
|
|
888
|
+
every domain and every in-bounds goal's cells against isl's membership.
|
|
889
|
+
- A cell set's dimensions are named apart from the loops of the access it is
|
|
890
|
+
built for, so that a ragged row's bound written in a loop called `a0`
|
|
891
|
+
reads back as that loop.
|
|
892
|
+
- A step that runs two updates of one cell at the same time is refused with
|
|
893
|
+
a `disjoint-writes` fact of the step, where a `monotone` fact named the
|
|
894
|
+
pair before. That fact is stated for isl only, with the reason
|
|
895
|
+
(`docs/lean-checks.md`, #201). A `monotone` cast is stated for Lean without
|
|
896
|
+
the pairs of updates of one cell, which need no order; a cast with no pair
|
|
897
|
+
left is one trivial part.
|
|
898
|
+
- Fewer indices than an array has axes, `A[i]`, are a row of it in a kernel
|
|
899
|
+
body, and an array of indices, `u[cell[c]]`, the cells it picks: a view a
|
|
900
|
+
called kernel takes, natively the row as an `Arr` sharing the buffer and
|
|
901
|
+
the gathered cells as a read-only copy, where both were refused as an
|
|
902
|
+
operation on the whole array. Used as a value, a view is still refused,
|
|
903
|
+
with the loop nest to write. A row of the cells a gather picks,
|
|
904
|
+
`u[cell][c]`, is `u[cell[c]]`, as numpy reads it. A table's row, `PHI[q]`,
|
|
905
|
+
reads natively as a read-only array, and as zeros out of range under a
|
|
906
|
+
false guard, as a cell does.
|
|
907
|
+
- In a program's body, `x.dom.size` (and `x.dom[0].size`) is the size of an
|
|
908
|
+
axis of the argument, for the arrays it makes; iterating `x.dom` is still
|
|
909
|
+
refused, as a loop whose trip count is an argument. A kernel's contract
|
|
910
|
+
reads a size of its declared domain off the bounds of an argument's
|
|
911
|
+
binders, where the argument was built over a domain of other names.
|
|
912
|
+
- A kernel that returns a value without declaring its sort, `-> Real`, is
|
|
913
|
+
refused when traced, naming the sort to declare; its value was dropped by
|
|
914
|
+
the compiled kernel.
|
|
915
|
+
- Two array parameters a kernel only reads may share storage, `dot(x, x)`
|
|
916
|
+
or the diagonal of a Gram matrix: reads do not race, and both runs read
|
|
917
|
+
the same values. The contract still refuses two that share storage where
|
|
918
|
+
the kernel writes either; a native call traces the body to know which
|
|
919
|
+
it writes, only when two of its arguments share storage.
|
|
920
|
+
- A schedule step that names no loop of the kernel lists its loops, and
|
|
921
|
+
names a called kernel's loop that was renamed when it was inlined:
|
|
922
|
+
`fill's loop n, inlined at ..., is n_0 here`.
|
|
923
|
+
|
|
924
|
+
### Fixed
|
|
925
|
+
|
|
926
|
+
- **An `ilp` loop around a sum runs its lanes in one loop of the sum
|
|
927
|
+
(#264).** A sum's domain named every loop around its statement, and loopy
|
|
928
|
+
opens a sum's loop inside every loop its domain names, so
|
|
929
|
+
`split("q", 3).tag(q_inner="ilp")` around `reduce_sum(... for b in
|
|
930
|
+
Fs.dom[q, j])` gave each lane a loop over `b` of its own, and the lanes
|
|
931
|
+
ran one after another. The domain now names only the loops its bounds
|
|
932
|
+
read: over a box, the loop over `b` holds the three lanes' updates, as
|
|
933
|
+
plain loopy writes it, while a triangle's `j < i + 1` keeps its sum in the
|
|
934
|
+
loop it reads, and a ragged row's length keeps every loop. The facts are
|
|
935
|
+
the term's and do not change. The launches of S1's "fields three at a
|
|
936
|
+
time" shape are now plain loopy's code up to loop names, and every other
|
|
937
|
+
shape's code is what it was; sumpy's tile on C in
|
|
938
|
+
`examples/layer_potential.py` runs the sources' loop once around its 16
|
|
939
|
+
targets, where it ran it once per target. Note 32 in
|
|
940
|
+
`docs/loopy-notes.md` has the details.
|
|
941
|
+
- **A program's zeros on a device** (#255, #261). A program zeroes each array
|
|
942
|
+
it makes in a statement of its own, in loops the lowering names (`i_2`,
|
|
943
|
+
`i_5`), which ran on every work item: `fused.tag(i="g.0")` was refused at
|
|
944
|
+
`C.zeros`, and the schedule that was timed tagged `i_2` and `i_5`, names the
|
|
945
|
+
file never wrote. The zeros' loop along an axis of the array now goes on
|
|
946
|
+
the axis of the loop that every statement storing the array stores it
|
|
947
|
+
along, so each work item zeroes the cells it stores, before it stores them,
|
|
948
|
+
and the work-item check asks about the zeros as about any statement.
|
|
949
|
+
Where a crossing is a call's and the zeros', the call's is named. Zeros
|
|
950
|
+
that cannot follow (the array stored along a split loop, or along loops on
|
|
951
|
+
two axes) stay on every work item, and the refusal says why and that
|
|
952
|
+
`contract` or `substitute` drops them (#289). grudge's weak gradient in one launch,
|
|
953
|
+
every node on a work item of its own, is now refused for the row the pass
|
|
954
|
+
along `r` reads from other work items, not for the zeros of `w`.
|
|
955
|
+
- On OpenCL, a complex number to a natural power below 100 is the products
|
|
956
|
+
numpy's power by an integer takes (`z * ((z * z) * (z * z))` for 5), never
|
|
957
|
+
pyopencl's `cdouble_powr`. loopy writes only the powers by 2, 3 and 4 as
|
|
958
|
+
products, and by 1 or 5, which a written-out series has, `exp(n log z)`:
|
|
959
|
+
a logarithm and an exponential for each, and not exact for `z**1`.
|
|
960
|
+
- A schedule's `ilp` or `vec` tag no longer has loopy warn, on every build,
|
|
961
|
+
that it falls back to its first scheduler, which it does for every kernel
|
|
962
|
+
with such a loop: the tag silences `v1_scheduler_fallback` on the kernel
|
|
963
|
+
it makes. The warning opened `python examples/assembly.py` and
|
|
964
|
+
`python examples/layer_potential.py` on a fresh checkout.
|
|
965
|
+
- An array of several axes read only through an index array,
|
|
966
|
+
`coords[cell[c, k], d]` with `nv` in no loop bound, is declared with its
|
|
967
|
+
strides, `stride:2,stride:1`, so that loopy indexes its rows; it was
|
|
968
|
+
declared without a shape, and loopy refused the read.
|
|
969
|
+
- An access with mixed subscripts that a statement reads twice over one
|
|
970
|
+
domain, `x[0, 0]` read for two entries of an inlined Jacobian, is one
|
|
971
|
+
place, decided by isl; it was `assumed`.
|
|
972
|
+
- A schedule's assumption is named `assume('nc % 4 == 0')` in the status of
|
|
973
|
+
the casts that rest on it (with lanky's `label`), where its whole id was.
|
|
974
|
+
- **Two statements' atomic updates of one cell asked loopy for a global
|
|
975
|
+
barrier.** `y[p] += a[p]` and `y[p + 1] += a[p]` with `p` on `l.0` race
|
|
976
|
+
on `y[p + 1]`, and after `atomic("y")` the schedule is legal and its
|
|
977
|
+
`buildable` fact holds, but the lowered kernel kept the source's order of
|
|
978
|
+
the two, so loopy refused to generate it (`MissingBarrierError`). Once an
|
|
979
|
+
array's updates are atomic, the order is drawn again from the dependences
|
|
980
|
+
the casts are checked against, and two updates no dependence joins need
|
|
981
|
+
no order (loopy's `no_sync_with`): libParanumal's lift onto the face nodes
|
|
982
|
+
(`examples/advection.py`).
|
|
983
|
+
|
|
984
|
+
- A write through a cell read from an array, `y[perm[i]] = x[i]`, no longer
|
|
985
|
+
has its distinct writes refuted by isl with two instances that write no
|
|
986
|
+
common cell. The fact is decided on the kernel's native runs instead. It is
|
|
987
|
+
refuted with two instances that write one cell on an input, the example
|
|
988
|
+
inputs first: `S0[c=0, k=2] and S0[c=1, k=1] both write y[3] on
|
|
989
|
+
example_inputs() (nc=2, nv=4), where cell[0, 2] = 3 and cell[1, 1] = 3`.
|
|
990
|
+
Where no input has such a pair, as for `y[i, p[i]]`, it is `tested`.
|
|
991
|
+
- The first run of a kernel with an array of four axes over one size, or a
|
|
992
|
+
triangle boxed over two, no longer prints loopy's `ParameterFinderWarning:
|
|
993
|
+
Unable to generate code to automatically find 'm' from 'u': nonlinear
|
|
994
|
+
expression`, a warning about an equation of the strides loopy's invoker
|
|
995
|
+
could not solve, while it found the size from the shape (note 28 in
|
|
996
|
+
`docs/loopy-notes.md`). Five constructs kernels written like sumpy's L2P
|
|
997
|
+
and grudge's sum-factorized passes need, a
|
|
998
|
+
`Where` array beside a ragged family, a `Where` of three binders, `**` with
|
|
999
|
+
a loop variable as the exponent in a sum of two binders and in a sum
|
|
1000
|
+
nested in another, four dense axes and `when(a + b == 0)`, were run three
|
|
1001
|
+
ways and work; `tests/test_demo_shapes.py` keeps them so.
|
|
1002
|
+
- A split sum adds its terms in order. `split("s", 256)` of a sum's loop
|
|
1003
|
+
left loopy to choose the order of the two loops it makes, and loopy ran
|
|
1004
|
+
the inner one outside, `for s_inner ... for s_outer`, adding every 256th
|
|
1005
|
+
term first: another sum in floating point, `2**53` where the native run
|
|
1006
|
+
adds `1 + 1 + 2**53` to `2**53 + 2`. Every schedule now gives loopy a sum's
|
|
1007
|
+
loops in order, outermost first, as it gives it each statement's nest.
|
|
1008
|
+
- `fuse` after a `split` or a `tile` of the producer's outer loop is no
|
|
1009
|
+
longer refused as unbuildable. The producer's domain is cut after the fused
|
|
1010
|
+
loops by name, not by position: loopy 2025.2 puts a split loop's two after
|
|
1011
|
+
the others, `[j, i_outer, i_inner]`, so
|
|
1012
|
+
`Schedule(squared_rows).split(i, 2).fuse("square2", "row_total")` read
|
|
1013
|
+
`buildable` refuted though both casts were decided
|
|
1014
|
+
([#192](https://github.com/xywei/loopty/issues/192)).
|
|
1015
|
+
|
|
1016
|
+
## [0.2.0] - 2026-10-11
|
|
1017
|
+
|
|
1018
|
+
The second release, on lanky 0.2.0, which checks each theorem's reading of
|
|
1019
|
+
its annotations and gives it a row of its own in the ledger, and whose `abs`
|
|
1020
|
+
of a term compares (lanky#94); loopty requires it, so that the ledgers its
|
|
1021
|
+
documentation shows are the ones an install prints. The rest is two waves of
|
|
1022
|
+
fixes where the compiled run computed otherwise than numpy, or did not build:
|
|
1023
|
+
subscripts, storage and names (#164: an index sum in a dividing subscript is
|
|
1024
|
+
64 bits wide, a narrow subscript stays in its type, complex `==` builds on
|
|
1025
|
+
OpenCL, a program's arrays are off the C stack, and every isl dimension
|
|
1026
|
+
loopty names has a name of its own), and arithmetic and messages (#169: a
|
|
1027
|
+
sum is computed as the body wrote it, `abs` of a real lowers in a guard, the
|
|
1028
|
+
compiled run is refused before it starts where the native run is refused,
|
|
1029
|
+
and loopy's refusal of a guard it cannot read names the fix). CI also tests
|
|
1030
|
+
loopty against the released lanky (#160). The README's Known limits section
|
|
1031
|
+
lists what is still open.
|
|
1032
|
+
|
|
1033
|
+
### Added
|
|
1034
|
+
|
|
1035
|
+
- **The names of isl dimensions** (`loopty.names`, #161). Every isl object
|
|
1036
|
+
loopty names the dimensions of keeps namedisl's invariants: each
|
|
1037
|
+
dimension has a name, no two share one, and none is named like a
|
|
1038
|
+
parameter. isl's reader breaks them silently, reading a tuple variable
|
|
1039
|
+
spelled like a parameter as that parameter (#110). `check_names` checks
|
|
1040
|
+
them where such an object is made, a statement's and a sum's domain when
|
|
1041
|
+
the term records it, the loops of a nest, an access relation (its cells
|
|
1042
|
+
are positional), the index tuples of a shape, a dense layout's map and a
|
|
1043
|
+
polyhedral domain's points, and raises `NamingError`, a bug of loopty's,
|
|
1044
|
+
naming the object and the clash. `rename` is the one way loopty renames
|
|
1045
|
+
dimensions and replaces parameters, all at once: composing a program
|
|
1046
|
+
(`compose.rename_set`), the binders of a nested sum in the lowering, and
|
|
1047
|
+
the native run fixing a parameter at the value it reads use it.
|
|
1048
|
+
|
|
1049
|
+
### Changed
|
|
1050
|
+
|
|
1051
|
+
- **Requires lanky 0.2.0** (`lanky>=0.2.0`, was `>=0.1.0`). Its ledger has
|
|
1052
|
+
a reading row under each theorem (the next entry), which the `lanky check`
|
|
1053
|
+
transcripts in the README, the quickstart and the examples' README show,
|
|
1054
|
+
and its `abs` of a term is lanky's `Abs`, so a comparison of it traces,
|
|
1055
|
+
`when(abs(k[i]) > 2)` and `127 == abs(c[i])` (lanky#94); the test of that
|
|
1056
|
+
(#165) no longer skips on an older lanky. `[tool.uv.sources]` still
|
|
1057
|
+
resolves lanky from `../lanky` in a checkout, and the built distributions
|
|
1058
|
+
ask for it from PyPI.
|
|
1059
|
+
- **A theorem in a kernel file has its reading under it** (lanky #91, #155).
|
|
1060
|
+
lanky now checks each claim's reading of its annotations: the annotations
|
|
1061
|
+
are run again as plain Python at drawn values, the term is evaluated at the
|
|
1062
|
+
same values, and the two have to agree. Each theorem gets a second fact,
|
|
1063
|
+
`tested python ... the term computes what the annotations compute`, on
|
|
1064
|
+
which its pass or proof rests. The `lanky check` transcripts of
|
|
1065
|
+
`examples/spmv.py` and `examples/travel.py`, each of which has a
|
|
1066
|
+
`scan_monotone`, show the new row, and are regenerated with
|
|
1067
|
+
`scripts/refresh_example_outputs.py`; `examples/README.md` says what the
|
|
1068
|
+
row is. A kernel's and a program's facts are loopty's, and have no such
|
|
1069
|
+
row: their trace has its own check, `trace-faithful`. (This entry was
|
|
1070
|
+
filed under 0.1.0 by mistake; it was merged after that release.)
|
|
1071
|
+
- CI also tests loopty as `pip install loopty` gets it: a second job installs
|
|
1072
|
+
it without `[tool.uv.sources]`, so with lanky from PyPI, and runs the suite
|
|
1073
|
+
and the worked example's `lanky check` and `loopty run` (#147).
|
|
1074
|
+
- loopy's refusal of an access under a guard its bounds check does not read,
|
|
1075
|
+
a product of loop variables, a guard computed in floating point or an
|
|
1076
|
+
array's entry, names the statement, where the body wrote it, the guard,
|
|
1077
|
+
and a guard over integers that keeps the access in bounds, which loopy
|
|
1078
|
+
reads, with a size as the body reads it: `with when(i <= y.dom.size - 5):`
|
|
1079
|
+
for `x[i + 4]` under `when(i * 0.5 < 2)` (#148). It is
|
|
1080
|
+
`loopty.refusal.GuardNotRead`, a `LoopyIndexError` still, with loopy's
|
|
1081
|
+
message at its end. A guard that calls a function no statement's value
|
|
1082
|
+
calls, which loopy never resolves, is refused with a `LoweringError`
|
|
1083
|
+
naming the fix, where loopy failed with `KeyError` and the function's
|
|
1084
|
+
name.
|
|
1085
|
+
- An array a program makes is off the C stack (#62). On the C target loopy
|
|
1086
|
+
declared it a variable-length array on the stack of the call, `double
|
|
1087
|
+
f[n];`, so the thread's stack bounded how large an intermediate could be,
|
|
1088
|
+
and a program whose intermediate was larger, eight megabytes on a thread
|
|
1089
|
+
with four, died with no diagnostic. `LoopyExecutor` now allocates it on
|
|
1090
|
+
the heap, zeroed, and passes it after the program's own arguments,
|
|
1091
|
+
`double *__restrict__ f`, and `emit_code` and `loopty run --emit-code`
|
|
1092
|
+
print it so (`lower.heap_temporaries`). The term and its schedules keep it
|
|
1093
|
+
a temporary, which `Schedule.substitute` computes away; an empty one stays
|
|
1094
|
+
where it takes no room, since loopy's C invoker cannot pass an empty
|
|
1095
|
+
array. The OpenCL target's global temporary is unchanged.
|
|
1096
|
+
|
|
1097
|
+
### Fixed
|
|
1098
|
+
|
|
1099
|
+
- A sum is computed as the body wrote it (#151). pymbolic builds `a - b` as
|
|
1100
|
+
`a + -1 * b` and `u - 1` as `u + -1`, the same nodes `a + -b` and `u + -1`
|
|
1101
|
+
build, and the trace, the plan and the interpreter read every such sum as
|
|
1102
|
+
the difference, which numpy computes otherwise: `c[i] + -a[i]` of an `int8`
|
|
1103
|
+
`a` is `c[i] + -128` at `a[i] = -128` natively and was `c[i] + 128`
|
|
1104
|
+
compiled, and `c[i] + -u[i]` of a `uint8` `u` adds `256 - u[i]` natively
|
|
1105
|
+
and subtracted `u[i]`, at every `u[i]` but zero. The trace builds every `-`
|
|
1106
|
+
the body writes as a `loopty.term.Difference`, a sum whose last term is
|
|
1107
|
+
subtracted, and any other sum is read as written. So `u[i] + -1` of an
|
|
1108
|
+
unsigned `u`, `k[i] - 2**31` and `k[i] + 2**31` of an `int32` `k`, `u[i] -
|
|
1109
|
+
-7`, `c[i] + -b[i]` and `b[i] - True` of a truth value `b`, which numpy
|
|
1110
|
+
refuses at every point and the compiled run computed as the other
|
|
1111
|
+
spelling, are `TraceError`s naming the fix, and `k[i] + -(2**31)` and
|
|
1112
|
+
`c[i] - -32768` of an `int16` `c` are computed in the type numpy computes
|
|
1113
|
+
them in. A numpy scalar subtracted is kept as written, where pymbolic negates it
|
|
1114
|
+
in its own type: `k[i] - np.uint8(3)` was `k[i] + 253` compiled. The
|
|
1115
|
+
smallest `int32` and `int64` are written as C writes `INT_MIN`,
|
|
1116
|
+
`(-2147483647 - 1)`: `-2147483648` is the negation of a `long` in C, and
|
|
1117
|
+
`k[i] + -2147483648` of an `int32` `k` was computed in 64 bits, never
|
|
1118
|
+
wrapping round as numpy does; `-9223372036854775808` is the negation of a
|
|
1119
|
+
constant no signed type holds, which GCC reads in 128 bits with a warning
|
|
1120
|
+
and clang as an `unsigned long`. That is on the C target, built with
|
|
1121
|
+
`-fwrapv`; OpenCL C has none, and its compilers fold `k[i] + (-2147483647 -
|
|
1122
|
+
1)` into `k[i] | (-2147483647 - 1)`, so there the literal is left as loopy
|
|
1123
|
+
writes it. A real less an integer keeps the sign of zero: `x[i] - 0` and
|
|
1124
|
+
`x[i] - i`, `-0.0` natively at `x[i] = -0.0` and `i = 0`, were `0.0`
|
|
1125
|
+
compiled, since pymbolic builds them as `x[i] + 0` and `x[i] + -1 * i`; the
|
|
1126
|
+
zero is written `-0.0`, and `i` is converted into the real's type before it
|
|
1127
|
+
is negated. A truth value subtracted, `k[i] - True`, which pymbolic takes as
|
|
1128
|
+
no operand, is a `TraceError` naming the integer, `k[i] - 1`. `k[i] - 2**63`
|
|
1129
|
+
and `b[i] - 2**63` of a truth value `b`, which numpy refuses in `int64`, are
|
|
1130
|
+
refused by the trace, naming a real, `2.0 ** 63`.
|
|
1131
|
+
- `abs` of a real in a guard lowers (#154): loopy's type inference visits a
|
|
1132
|
+
statement's value and never its guard, so `abs`, which only the guard
|
|
1133
|
+
called, was never resolved, and loopy failed with `KeyError: 'abs'`. In a
|
|
1134
|
+
guard it is written `x < 0 ? -x : (x == 0 ? 0 : x)`, which is numpy's
|
|
1135
|
+
`abs(-0.0)` too, `0.0`.
|
|
1136
|
+
- Loop arithmetic beside a numpy integer whose type does not hold it at a
|
|
1137
|
+
point of the loops, `u[i] + (i - 3)` of a `uint32` `u` at `i = 0`, `a[i] *
|
|
1138
|
+
(i * 100)` of an `int8` `a` from `i = 2`, and a loop variable stored into an
|
|
1139
|
+
`int8` array from `i = 128`, is refused by numpy natively there, and the
|
|
1140
|
+
compiled run computed it in C's type (#153). Where the call's sizes reach
|
|
1141
|
+
such a point, isl finds it, and the compiled run is refused before it
|
|
1142
|
+
starts (`loopty.refusal.RefusedNatively`, an `OverflowError`), naming the
|
|
1143
|
+
point and `(i - 3) * np.int64(1)` as the fix. Arithmetic isl cannot read,
|
|
1144
|
+
or a ragged row's loop, is left to the native refusal (#166). A sum's body
|
|
1145
|
+
is asked about where the generator's `if` clause holds, which skips it
|
|
1146
|
+
natively: `reduce_sum(a[j] + j for j in a.dom if j < 100)` of an `int8`
|
|
1147
|
+
`a` is computed at any size. A program's call after a checked point is
|
|
1148
|
+
refused once the run has shown that the points before it passed, whatever
|
|
1149
|
+
a point after it says; where one before it failed, that point's error is
|
|
1150
|
+
raised, as natively. A literal stored into an integer array that does not
|
|
1151
|
+
hold it, `y[i] = 300` of a `uint8` `y`, is a `TraceError`, as #141's
|
|
1152
|
+
literals are; under a guard, which masks the store, it is refused where the
|
|
1153
|
+
call's sizes reach a point of the guard, and `with when(i < 0): y[i] = 300`
|
|
1154
|
+
stores nothing in either run.
|
|
1155
|
+
- A regression test for #152, which #156 fixed: a literal beside a loop
|
|
1156
|
+
variable under a double power in a single-precision statement is written as
|
|
1157
|
+
an integer, `(double) (i + 16777216)`, where loopy wrote `1.0f` and C
|
|
1158
|
+
rounded the loop variable past `2**24`.
|
|
1159
|
+
- A sum of loop variables and sizes in a dividing subscript is computed in
|
|
1160
|
+
64 bits (#149). The plan leaves such a sum in 32 bits, as loopy computes a
|
|
1161
|
+
loop bound, and a remainder or a floor division in a subscript used the
|
|
1162
|
+
wrapped value once the sum passed `2**31`: `x[(i + k) % m]` at `k = 2**31 -
|
|
1163
|
+
1` read cell `(-2**31) % 3` at `i = 1`, where numpy reads `2**31 % 3`, and
|
|
1164
|
+
loopy wrote `x[(i + n) // 2]`, which it reads as affine with a division,
|
|
1165
|
+
as `x[(i + n) / 2]`. In a subscript loopy does not read as affine with no
|
|
1166
|
+
division the sum's first operand is widened now,
|
|
1167
|
+
`x[loopty_mod_int64((int64_t) (i) + k, (int64_t) (m))]`, and `abs` of one
|
|
1168
|
+
takes its sign from bit 63. The same sum in a value or a guard on the
|
|
1169
|
+
loops is still 32 bits wide (#162).
|
|
1170
|
+
- A subscript over a narrow numpy integer type is computed in it, as numpy
|
|
1171
|
+
computes it (#157). The conversion back into `int8`, `uint8`, `int16` or
|
|
1172
|
+
`uint16` that an operation in such a type gets elsewhere (#122) was left
|
|
1173
|
+
out of a subscript, so `x[s + i]` of a `uint8` `s`, which loopy reads as
|
|
1174
|
+
affine, read cell 256 where numpy takes `250 + 6` round to 0, and
|
|
1175
|
+
`x[abs(s) % 7]` of an `int8` `-128` read cell 2, where numpy's `abs` keeps
|
|
1176
|
+
`-128`, whose remainder is 5. It is kept now, `x[(uint8_t) (s + i)]`, and
|
|
1177
|
+
loopy's reader declines the cast, so the subscript is written as it is.
|
|
1178
|
+
- `==` and `!=` of complex values build on the OpenCL target (#158).
|
|
1179
|
+
pyopencl's complex types are structs, which OpenCL C does not compare, and
|
|
1180
|
+
loopy wrote `z[i] == w[i]` as it is, so the kernel did not build. They are
|
|
1181
|
+
compared part by part, as numpy compares, in the complex type of the two,
|
|
1182
|
+
a real operand with a zero imaginary part: `cdouble_real(z[i]) ==
|
|
1183
|
+
cdouble_real(w[i]) && cdouble_imag(z[i]) == cdouble_imag(w[i])`. The C
|
|
1184
|
+
targets compare C99's complex types with C's operators, as before.
|
|
1185
|
+
- The index tuples of a shape whose size is named `i0`, `idx.to_set((Fin[i0],
|
|
1186
|
+
...))`, had no points: their dimensions were named `i0, i1, ...` whatever
|
|
1187
|
+
the sizes are called, and isl read the dimension `i0` as the size, `0 <= i0
|
|
1188
|
+
< i0`. They are named clear of the sizes now, as a polyhedral domain's
|
|
1189
|
+
already were (#161).
|
|
1190
|
+
- A sum by a binder named like a size or an integer parameter of the kernel,
|
|
1191
|
+
`reduce_sum(x[j] for j in x.dom)` over `x: Arr[Fin[j], Real]`, is refused
|
|
1192
|
+
at the trace, naming the fix (#161). Its domain was `{ [j] : 0 <= j < j }`,
|
|
1193
|
+
a set with a well-named dimension and no points, which the name check
|
|
1194
|
+
could not see: the compiled sum was `0` where numpy's was not, the
|
|
1195
|
+
in-bounds fact of `x[j + 5]` was decided over the empty domain, and a sum
|
|
1196
|
+
by `i` inside a loop bounded by a size `i` did not build. A loop variable
|
|
1197
|
+
so named was already given a name of its own. `flow.domain_set` and a
|
|
1198
|
+
polyhedral domain's points now also refuse a dimension named like a size
|
|
1199
|
+
their bounds meant as a parameter (`check_names(..., meant=...)`).
|
|
1200
|
+
|
|
9
1201
|
## [0.1.0] - 2026-10-09
|
|
10
1202
|
|
|
11
1203
|
The first release on PyPI, where only the 0.0.1 placeholder that reserved the
|