Skip to content

Fix CI correctness defects and excessive stencil compilation - #780

Open
jpsamaroo wants to merge 19 commits into
masterfrom
codex/fix-cpu-ci-and-stencil-compilation
Open

jpsamaroo wants to merge 19 commits into
masterfrom
codex/fix-cpu-ci-and-stencil-compilation

Conversation

@jpsamaroo

@jpsamaroo jpsamaroo commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

CI failures include incorrect LU solve results, factorization solves falling back to local arrays on Julia 1.14, Finch multiplication with CSC destinations, and logging test failures. CPU/OpenCL stencil tests also spend minutes compiling new variants for worker-placement patterns, contributing to job cancellations.

This PR fixes those source/test defects and makes the selected Distributed transport consistent across the harness and extensions. The changes remain separated into 19 commits for review.

  • Match LU pivot-vector blocks to the returned factors' row blocks, including NoPivot and buffered non-square tile layouts. Add solve regressions with explicit tile sizes that differ from AutoBlocks.
  • Implement LinearAlgebra's factorization-based allocation hook so Julia 1.14 solves retain distributed right-hand sides/results.
  • Handle CSC destinations in Finch tile multiplication, including accumulation into existing destination values.
  • Resolve ArrayDomain indexing against Julia 1.14 SparseArrays' new vararg CSC indexing fallback, preserving sparse results and unblocking distribute(::SparseMatrixCSC).
  • Preserve the first unexpired LogWindow event, compare elapsed ages safely across UInt64 timestamp wrap, and collect dashboard logs before checking the aggregator's HTTP endpoint.
  • Select DistributedNext for Julia 1.10/1.11 CPU jobs and the Julia 1.11 OpenCL job. Propagate the transport preference consistently through Dagger, MemPool, TimespanLogging, test imports, and worker projects. Declare DistributedNext directly in the test project: worker startup imports it in Main, where Dagger's transitive dependency is insufficient. GPU/Python extensions now use Dagger's transport functions, fixing OpenCL workers incorrectly registering their processors with owner 1 under DistributedNext.
  • Reduce stencil compilation by moving each halo region's construction behind a specialized non-inlined helper, keeping dependency metadata out of submission signatures with concrete Read{Any} wrappers, and giving received CPU Wrap regions the same view representation as local halos. Numerical execution still specializes on actual array types. Add mixed local/received halo regression coverage and backend/dimension progress messages.
  • Exclude only the problematic 4D stencil from MPI suites; keep shared boundary-condition and 3D coverage. Ordinary CPU/OpenCL suites retain 4D.
  • Await scheduler cleanup before removing test workers, and make sparse benchmark fixtures reproducible through fixed placement and seeded inputs.

Validation performed locally:

Environment / check Result
Julia 1.10.12, full CPU MPI suite, four ranks with two threads each 1,489 / 1,504 / 1,501 / 1,501 assertions passed (5,995 total), 5m38s
Julia 1.11.9, full PoCL GPU test body, driver plus three single-thread workers, DistributedNext 250 passed, 21 existing broken, 5m13s through the actual test-project harness with global environment hidden
Same Julia 1.11 topology, shared CPU and OpenCL stencil suites 126 passed, 5m44s
Julia 1.11.9, MPI × OpenCL, two ranks with two threads each 155 / 158 passed, 6m13s
Apple M2, macOS 26.7, Julia 1.13.2 with coverage, three two-thread workers CPU stencils: 63 passed; iterative solvers: 297 passed
Same Apple Silicon host, single process with --threads=auto and coverage LU: 136 passed, including NoPivot cases that failed in macOS CI
Julia 1.14 development build Solve: 44 passed; LU: 136 passed
Julia 1.12.4 allocation regression suite All 24 checks passed with unchanged bounds
Julia 1.10.12 and 1.11.9, copies of the actual test project, global environment hidden, DistributedNext with three two-thread workers Processor and preference tests: 32 assertions passed per version; removing the direct transport dependency reproduces the GHA worker-startup failure
Current Julia 1.14.0-DEV.3754, test-project harness Domain utilities: 17 passed; array allocation/distribute: 981 passed
TimespanLogging, Julia 1.13.2 All 79 assertions passed, including deterministic timestamp-wrap regression coverage

On the M2 host, the cold CPU stencil suite fell from 310s to 117s despite ten added regression assertions; 4D fell from 200s to 31s. In the Julia 1.11 three-worker topology, CPU 4D fell from approximately 720s to 49s. These are end-to-end cold test timings; the original M2 4D run overlapped part of a separate LU check. The OpenCL GPU and stencil test bodies above ran in separate fresh processes, so their combined duration excludes dependency installation and is not an end-to-end CI measurement. Local PoCL used POCL_MAX_PTHREAD_COUNT=4 to bound host resource use; that setting was not added to CI.

Allocation measurements used Julia 1.12.4, fully awaited/pinned fixtures, ten warmups followed by GC, and the minimum of five Base.gc_num() deltas:

CPU fixture Before allocations / bytes After allocations / bytes
2D stencil 7,321 / 374,880 7,104 / 362,320
4D stencil 217,684 / 15,401,096 202,918 / 13,964,416

No CI timeouts or allocation bounds were increased.

Remaining limitations: Finch 1.6.0 has a separate unsynchronized global compilation-cache Dict; a Finch-only concurrent reproducer fails while its serial control passes. No Dagger-side locking workaround is included. A sporadic DistributedNext unknown-socket diagnostic still occurs during worker shutdown after passing assertions, and one stdlib Distributed run lost a worker without a useful crash traceback; neither is claimed fixed. The broader macOS CI slowdown is also unresolved: original and fixed iterative-solver controls both finish in about two minutes on the M2 host, versus 33 minutes in the GitHub job. A complete GitHub-hosted macOS job has not yet been rerun with these changes.

Three earlier benchmark jobs also reported regressions independently of package loading: Distributed matmul allocations, Distributed+Threads Clamp stencil timing, and MPI+Threads LU solve timing. The matmul allocation discrepancy was approximately 123% in the first run but approximately 1.5% in its confirmation run; the two timing comparisons did repeat in CI. These are not claimed fixed, and no benchmark thresholds were changed.

MultiEventLog aggregators run at collection, so emitting one task never started the renderer and curl failed to connect. Enable the logging runtime, collect the batch, and assert the actual HTTP status and HTML response. Keep the five-second window semantics and disable logging afterward.

Validation: DaggerWebDash Basics passes all four assertions on Julia 1.12.4 with two threads.
LogWindow used the first retained index as a deletion count, dropping an additional valid event from mixed-age batches. Remove only the preceding entries and preserve alignment across consumer columns.

Validation: all 77 TimespanLogging assertions pass on Julia 1.12.4 with four threads, including mixed, entirely retained, and entirely expired batches.
Use DistributedNext for the Julia 1.10/1.11 multiprocess CI jobs, where stdlib Distributed raises lock-must-be-held concurrency errors. Configure all three transport preferences before imports, use the same transport for test macros and worker management, and preserve it after preference tests. Workers created inside scheduler/processor tests now inherit the active test project.

Validation: Julia 1.10.12, two driver threads and three workers with two threads each: thunk, scheduler, processors, options, task queues, datadeps (2143 passed), reuse (12615 passed), and preferences all pass. Existing broken tests remain reported as broken. Newer Julia jobs retain stdlib transport coverage.
LinearAlgebra now passes a Factorization to _zeros instead of its column count. Forward that signature to Dagger existing allocator so distributed RHS solves keep their storage and dispatch. Cover vectors and matrices, numerical results, RHS preservation, and allocation via the new protocol on every supported Julia.

Validation: on Julia 1.14.0-DEV.3709 the Cholesky reproducer changes from Vector to DVector; all 25 allocation-suite checks pass with existing bounds. Python integration also passes all six checks on this nightly; the independent Pkg CommandSpec precompile failure is fixed upstream in JuliaLang/Pkg.jl#4863.
NoPivot allocated an automatically partitioned identity pivot vector, while row-swap kernels assume its blocks match the factors. This corrupted solves depending on processor count. Rectangular tiles also left pivoted LU pivots in the temporary square-panel layout. Allocate or repartition pivots to the returned factors row blocking.

Validation: Julia 1.11.9 with four threads passes 136 LU and 44 solve assertions, including real/complex, square/rectangular tile layouts, partial edge tiles, and vector/matrix RHS. A deterministic old-layout reproducer has solution error 0.5249 versus 2.09e-16 after alignment.

Memory: Julia 1.11.9, identical dependencies, pinned 16x16 fixture with 8x8 tiles, 10 warmups then GC and minima over five runs. NoPivot: 9464 allocations / 441440 bytes before, 9663 / 448496 after (two required pivot tiles instead of the incorrect single automatic tile). RowMaximum: 10937 / 503104 before, 10985 / 504384 after. Existing allocation bounds are unchanged.
Sparse inputs still used arbitrary placement, allowing driver-local allocation reports to change with ownership. Use the existing CPU Distributed assignment helper and seeded input generators in setup and capability probes. MPI/GPU keep arbitrary placement; operations, result placement, samples, and regression thresholds are unchanged.

Validation: Julia 1.12.4 with three one-thread workers runs SpMV, CG, and SpGEMM benchmark leaves with three samples each at N=64 and block size 16. Repeated seeded matrices collect identically and have the same balanced 4/4/4/4 tile-owner layout. Smoke allocation minima: SpMV 54387 / 2454224 bytes, CG 623058 / 27935464, SpGEMM 69602 / 3083344; these remain process-local measurements.
Out-of-place Finch GEMM can start with a host CSC destination from similar. The fallback sent Finch tensors through generic collect and failed because Tensor has no length. Build the Finch result and replace the sparse wrapper storage; materialize an existing non-Finch destination before nonzero-beta accumulation. Remove the obsolete test LOAD_PATH injection since workers already inherit the active project.

Validation: Julia 1.11.9, one driver thread and three one-thread workers: all 32 GEMM and 16 SpMV checks pass, including new mixed-storage beta checks. A separate two-thread run exposes Finch 1.6.0 global LogicExecutor.codes Dict races; no serialization workaround is introduced.

Memory: native 2x2 Finch GEMM with nonzero beta, Julia 1.11.9, 10 warmups, GC, minimum over five measurements: 1530 allocations / 65472 bytes before and after. The previously failing CSC-destination path has no valid baseline allocation measurement.
The harness removed worker sockets one second after requesting scheduler shutdown, racing cleanup RPCs and logging late transport errors. Wait on the existing scheduler-exit condition instead of sleeping.

Validation: Julia 1.11.9 with a one-thread driver and three two-thread workers passes 131 thunk and 30 processor assertions and exits without the previously observed unknown-socket error. Julia 1.10.12 with four threads and no initial workers passes 120 thunk assertions and exits cleanly. Existing broken checks remain reported.
The cold 4D case generates very large planning signatures and leaves peer ranks waiting while LLVM compiles. Disable only this case for CPU and GPU MPI entry points, retaining the existing backend-specific high-dimensional skips and all non-MPI coverage.

Validation: Julia 1.10.12, four MPI ranks with two threads each: all 52 remaining stencil assertions pass on every rank (211 seconds).
Cancelled jobs previously stopped at a single combined stencil-suite message, hiding whether CPU execution, GPU execution, or high-dimensional compilation was active. Emit progress at backend and high-dimensional case boundaries, and record the local PoCL profiling evidence.

Validation: the shared suite passes 53 assertions on PoCL, including 3D and 4D; this change only adds test progress messages.
A 4D halo unrolls 80 regions. Inlining host view construction and kernel-launch bodies into every tuple element gives LLVM a huge function to optimize. Keep the typed tuple but put construction behind a specialized non-inlined per-region barrier.

Julia 1.11.9 PoCL: isolated cold 4D stencil 114.75s -> 47.35s; full shared GPU suite 283.31s -> 215.98s, all 53 assertions pass. Four Julia 1.10.12 MPI ranks also pass all 52 remaining assertions each.

Allocation profiling: 10 warmups, GC, minimum of 5 gc_num deltas. CPU allocations/bytes unchanged: 2D 11/1536, 3D 29/6176, 4D 86/29264. PoCL unchanged: 2D 533/63712, 3D 1247/150704, 4D 2696/337968. Inferred return types are unchanged for all six fixtures.
With DistributedNext selected, direct stdlib Distributed imports report myid() == 1 on every worker. OpenCL workers 2 and 3 consequently registered GPU owner 1. Import the existing functions from Dagger in GPU and Python extensions so registration and transfers use the same registry as the scheduler.

Validation: Julia 1.11.9, DistributedNext, two two-thread workers: OpenCL ownership and distributed GPU dataflow 51 assertions pass; Python ownership 6 and Python numerical integration 6 assertions pass. The OpenCL probe logged a late transport socket error during teardown after numerical checks passed; this change addresses ownership and RPC selection.
Local DTask vs remote Chunk neighbors (including Chunk processor/scope
parameters) specialized each submission on placement. Wrap neighbor
metadata in concrete Read{Any}; execution still specializes on actual
arrays. Normalize received CPU Wrap regions to full-range views so local
and remote regions also share one concrete halo tuple/kernel signature.

The original Julia 1.11 three-worker CPU 4D case took 720 seconds; the
final DistributedNext run took 49 seconds. Both CPU and PoCL shared suites
pass (126 assertions, 344 seconds total). Four Julia 1.10 MPI ranks each
pass all 62 assertions through 3D. All 24 allocation bounds pass unchanged.
A late DistributedNext unknown-socket teardown diagnostic remains separate
from the passing numerical suites.

After 10 warmups and GC, minimum of five Base.gc_num deltas on Julia 1.12:
2D: 7,321 allocations / 374,880 bytes -> 7,104 / 362,320.
4D: 217,684 allocations / 15,401,096 bytes -> 202,918 / 13,964,416.
A mixed local/received halo fixture also becomes concretely inferred and
checks value correctness, homogeneous representation, and retained aliasing.
GPU extensions now honor the selected transport. The three-worker CPU and PoCL stencil suites pass 126 assertions with DistributedNext, and the separate functional GPU probe passes 51 assertions. Keep the Julia 1.11 OpenCL job consistent with the CPU job transport choice; newer Julia jobs retain stdlib Distributed coverage.
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Dagger benchmarks: dirty vs master

Counts are benchmark metrics (time, allocations, bytes). Timing changes require non-overlapping median ± IQR bands; both revisions need at least five timed samples, and timing regressions need an independent confirmation run.

Backend Regressions Improvements Within noise Inconclusive time Suites
Threads 0 3 9 3 4/4
Distributed 3 0 3 0 4/4
Distributed+Threads 0 1 0 4 4/4
MPI 0 1 0 0 4/4
MPI+Threads 1 1 15 3 4/4
Total 4 6 27 10 20/20

Regressions across all backends

Backend Benchmark Metric Change
Distributed linalg/dagger/N=1024 (block 128)/syrk (A'*A) allocs +121.8%
Distributed linalg/dagger/N=1024 (block 128)/syrk (A'*A) memory +82.7%
MPI+Threads linalg/dagger/N=1024 (block 512)/solve (A\b via lu) time +57.8%
Distributed array/dagger/N=1024 (block 128)/add (X + X) memory +27.6%

Improvements across all backends

Backend Benchmark Metric Change
Threads array/dagger/N=256 (block 128)/broadcast (X .+ 1) time -82.4%
MPI+Threads linalg/dagger/N=256 (block 128)/matmul (A*A) time -61.1%
MPI array/dagger/N=1024 (block 512)/norm time -47.4%
Threads stencil/dagger/N=256 (block 256)/multi-expr time -42.9%
Distributed+Threads stencil/dagger/N=1024 (block 512)/neighbors (Clamp) time -41.5%
Threads array/dagger/N=256 (block 256)/broadcast (X .+ 1) time -25.9%
Threads (1 process × 4 threads) — 0 regressions, 3 improvements
Suite Regressions Improvements Within noise Inconclusive time
array 0 2 5 0
linalg 0 0 3 0
sparse 0 0 0 0
stencil 0 1 1 3

Improvements

Benchmark Metric Change
array/dagger/N=256 (block 128)/broadcast (X .+ 1) time -82.4%
stencil/dagger/N=256 (block 256)/multi-expr time -42.9%
array/dagger/N=256 (block 256)/broadcast (X .+ 1) time -25.9%

Within noise

Benchmark Metric Change
array/dagger/N=1024 (block 512)/add (X + X) time +50.0%
linalg/dagger/N=256 (block 128)/matvec (A*x) time +40.8%
array/dagger/N=256 (block 128)/reduce (sum) time +30.3%
stencil/dagger/N=256 (block 128)/multi-expr time +26.5%
linalg/dagger/N=1024 (block 512)/matvec (A*x) time -32.1%
array/dagger/N=1024 (block 512)/reduce (sum) time -33.1%
array/dagger/N=1024 (block 128)/broadcast (X .+ 1) time -33.5%
linalg/dagger/N=256 (block 128)/matmul (A*A) time -43.6%
array/dagger/N=256 (block 128)/map (sin.(X)) time -67.5%

Inconclusive timing changes

Benchmark Metric Change
stencil/dagger/N=1024 (block 512)/update (+) time +43.1%
stencil/dagger/N=1024 (block 128)/multi-expr time +42.9%
stencil/dagger/N=1024 (block 512)/multi-expr time +33.9%
Distributed (4 processes × 1 thread) — 3 regressions, 0 improvements
Suite Regressions Improvements Within noise Inconclusive time
array 1 0 0 0
linalg 2 0 2 0
sparse 0 0 1 0
stencil 0 0 0 0

Regressions

Benchmark Metric Change
linalg/dagger/N=1024 (block 128)/syrk (A'*A) allocs +121.8%
linalg/dagger/N=1024 (block 128)/syrk (A'*A) memory +82.7%
array/dagger/N=1024 (block 128)/add (X + X) memory +27.6%

Within noise

Benchmark Metric Change
linalg/dagger/N=1024 (block 128)/lu time +124.9%
linalg/dagger/N=1024 (block 128)/syrk (A'*A) time +62.4%
sparse/dagger/N=1024 (block 64)/cg solve (laplacian) time +50.8%
Distributed+Threads (2 processes × 2 threads) — 0 regressions, 1 improvements
Suite Regressions Improvements Within noise Inconclusive time
array 0 0 0 2
linalg 0 0 0 0
sparse 0 0 0 0
stencil 0 1 0 2

Improvements

Benchmark Metric Change
stencil/dagger/N=1024 (block 512)/neighbors (Clamp) time -41.5%

Inconclusive timing changes

Benchmark Metric Change
stencil/dagger/N=1024 (block 512)/neighbors (Wrap) time +71.0%
array/dagger/N=1024 (block 512)/broadcast (X .+ 1) time +65.1%
array/dagger/N=1024 (block 512)/transpose (permutedims) time +52.3%
stencil/dagger/N=1024 (block 512)/neighbors (Pad) time +49.3%
MPI (4 ranks × 1 thread) — 0 regressions, 1 improvements
Suite Regressions Improvements Within noise Inconclusive time
array 0 1 0 0
linalg 0 0 0 0
sparse 0 0 0 0
stencil 0 0 0 0

Improvements

Benchmark Metric Change
array/dagger/N=1024 (block 512)/norm time -47.4%
MPI+Threads (2 ranks × 2 threads) — 1 regressions, 1 improvements
Suite Regressions Improvements Within noise Inconclusive time
array 0 0 8 1
linalg 1 1 3 2
sparse 0 0 0 0
stencil 0 0 4 0

Regressions

Benchmark Metric Change
linalg/dagger/N=1024 (block 512)/solve (A\b via lu) time +57.8%

Improvements

Benchmark Metric Change
linalg/dagger/N=256 (block 128)/matmul (A*A) time -61.1%

Within noise

Benchmark Metric Change
stencil/dagger/N=1024 (block 512)/assign (const) time +228.6%
array/dagger/N=256 (block 128)/norm time +212.9%
array/dagger/N=256 (block 128)/broadcast (X .+ 1) time +101.5%
array/dagger/N=1024 (block 512)/norm time +84.0%
stencil/dagger/N=256 (block 256)/neighbors (Reflect) time +61.8%
array/dagger/N=256 (block 128)/map (sin.(X)) time -35.2%
linalg/dagger/N=256 (block 128)/syrk (A'*A) time -37.3%
linalg/dagger/N=1024 (block 128)/cholesky time -37.5%
linalg/dagger/N=256 (block 128)/matvec (A*x) time -39.4%
array/dagger/N=1024 (block 512)/reduce (sum) time -41.2%
stencil/dagger/N=256 (block 128)/neighbors (Wrap) time -41.7%
array/dagger/N=1024 (block 512)/broadcast (X .+ 1) time -44.4%
array/dagger/N=256 (block 128)/transpose (permutedims) time -51.0%
array/dagger/N=1024 (block 512)/alloc (rand) time -57.9%
stencil/dagger/N=256 (block 256)/assign (const) time -62.8%

Inconclusive timing changes

Benchmark Metric Change
linalg/dagger/N=1024 (block 512)/matvec (A*x) time +313.3%
array/dagger/N=256 (block 128)/reduce (sum) time +132.2%
linalg/dagger/N=1024 (block 512)/cholesky time +47.9%

Full results and plots (download the benchmark-results-* artifacts).

DistributedNext starts workers with using DistributedNext in Main. Its presence as a transitive Dagger dependency is insufficient there, so direct runtests.jl invocations failed before any tests ran. Add it to test/Project.toml. With global environments hidden, removing this declaration reproduces the exact GHA startup error; the actual processor and preference test bodies pass 32 assertions each on Julia 1.10 and 1.11 with three two-thread workers.
A fresh GHA runner's time_ns counter was only 104 seconds old when the
regression fixture subtracted 120 seconds. The wrapped timestamp looked
newer than every real event. An absolute cutoff also underflows whenever
uptime is shorter than the retention window. Compare elapsed UInt64 ages
and use the retained-prefix count to return when nothing expired.

The full TimespanLogging suite passes all 79 assertions on Julia 1.13,
including a deterministic wrap fixture that the previous method retains
incorrectly. After 10 warmups and GC, minimum of five 1,000-call gc_num
deltas on Julia 1.12.4: 9,000 allocations / 528,000 bytes before versus
6,000 / 496,000 after (9 -> 6 allocations per retained-event update).
SparseArrays now has a vararg getindex(::AbstractSparseMatrixCSC, I...)
fallback. Dagger's AbstractArray x ArrayDomain method constrains the other
argument, making distribute(sparse_matrix, Blocks(...)) ambiguous. Add the
intersection method in the sparse extension and retain native CSC slicing.

Current Julia 1.14.0-DEV.3754 passes 17 domain assertions and all 981 array
allocation assertions, including the sparse distribute case that failed in
both nightly GHA jobs. The new test checks inferred sparse results, values,
and empty-domain behavior. With 10 warmups plus GC and minimum of five
1,000-call gc_num deltas on Julia 1.12.4, the old generic indexing body and
the new specialization both use 6,000 allocations / 1,504,000 bytes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant