Repository navigation
Conversation
| const NUMERIC_ABC_OPTS = Options(subtree_budgets = [8192, 16384], regime_c_width = 16, regime_c_rows = 128, | ||
| subtree_parallelism = 0) | ||
| subtree_parallelism = 0, subtree_max_fronts = 32) |
There was a problem hiding this comment.
nit (non-blocking): subtree_max_fronts = 32 changes the regime mix of the "all three regimes" fixture on CPU, CUDA and AMDGPU too, and nothing says why Metal needed it (the override in the extension does not touch these explicit options). Add the reason in the comment above the constant; if it is a Metal-only need, condition it on the backend rather than changing the fixture for everyone.
| @@ -0,0 +1,294 @@ | |||
| # Metal Optimization Plan | |||
There was a problem hiding this comment.
nit (non-blocking): a second top-level plan document is not part of the repository layout (AGENTS.md "Layout": PLAN.md TASKS.md RESEARCH.md AGENTS.md), and its "Execution Record" is exactly the material that belongs in the task's Report block in TASKS.md (measurements, deviations, follow-ups). Suggest moving this file under docs/src/man/ (as a Metal backend note) or bench/, and copying the Execution Record into the Report.
| CUDACore = "bd0ed864-bdfe-4181-a5ed-ce625a5fdea2" | ||
| Krylov = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7" | ||
| MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" | ||
| Metal = "dde4c033-4e86-420c-a63e-0dd931031962" |
There was a problem hiding this comment.
nit (non-blocking): new weak dependency Metal (compat 1.11) needs its one-line justification in the Report (AGENTS.md "Session rules"), and a note on why 1.11 is the floor (the offset-aware MPSMatrix(buffer, desc, offset) constructor and the MPSMatrixDecompositionCholesky wrapper, presumably).
| function SparseDirectSolver._offset_potrf_plan(::MetalBackend, factor::MtlVector{Float32}, p0, f, w, | ||
| status::MtlVector{Float32}) | ||
| w >= MPS_POTRF_MIN_WIDTH || return nothing | ||
| desc = MPSMatrixDescriptor(w, w, sizeof(Float32) * f, Float32) | ||
| panel = MPSMatrix(factor, desc, (p0 - 1) * sizeof(Float32)) | ||
| kernel = MPSMatrixDecompositionCholesky(Metal.device(), false, w) | ||
| return MetalPotrfPlan(panel, kernel, Base.unsafe_convert(MTLBuffer, status)) |
There was a problem hiding this comment.
Status buffer overrun / aliasing of aux. The MPS status buffer handed to MPSMatrixDecompositionCholesky is N.aux, an MtlVector{Float32} of length nb = 1 (4 bytes, storage.jl:235). MPS writes an MPSMatrixDecompositionStatus, an NSInteger enum (8 bytes on arm64; the prototype in bench/metal_mps_proto.jl:18 correctly allocates MtlArray{MPS.MPSMatrixDecompositionStatus, 0} for it). Every wide front therefore writes 8 bytes into a 4-byte buffer. Page-granular buffer allocation hides it today, but it is an out-of-bounds device write, it clobbers aux (documented in storage.jl:139 as the max |aᵢⱼ| statistic, copied to the host by host_numeric), and the status is never read anyway.
Fix: give the plan its own status storage allocated once at allocate_numeric time — e.g. one MtlVector{MPS.MPSMatrixDecompositionStatus}(undef, 1) (or a UInt8 buffer of sizeof(MPSMatrixDecompositionStatus)) owned by MetalPotrfPlan (or one shared buffer stored in the plan and passed instead of aux), and drop the vendor_status/aux plumbing through _factor_front_c!/_factor_panel_c!. Keep the buffer referenced from the plan so the MTLBuffer stays alive.
| function SparseDirectSolver._offset_potrf_info!(::MetalBackend, plan::MetalPotrfPlan, info, idx) | ||
| fill!(view(info, idx:idx), Int32(0)) | ||
| cmdbuf = MPSCommandBuffer(global_queue(Metal.device())) | ||
| encode!(cmdbuf, plan.kernel, plan.panel, plan.panel, plan.status) | ||
| commit!(cmdbuf) |
There was a problem hiding this comment.
Allocation inside the numeric phase (must be recorded). This path creates an MPSCommandBuffer wrapper per wide front per (re)factorization; METAL_OPTIMIZATION_PLAN.md measures about 2.1 MB of host allocation per refactorization. AGENTS.md forbids allocation inside the numeric phase, and T31's own owner note (#53) is specifically about making the Metal/oneAPI dense path allocation-free per call. It is acceptable as a documented, measured exception only if the Report lists it as a deviation and a follow-up issue exists (the PR says "Follow-up issues opened: none"). Please open the follow-up (reusable command-buffer encoding) and cite it in the Report, or encode all wide fronts of a level into one command buffer created per launch to cut the count.
Also worth a comment here: ordering with the KA launches relies on Metal.jl's KA kernels and this command buffer sharing global_queue(device()); say so, since a future change to a private queue would silently break the dependency on the preceding assembly kernels and the following ka_chol_check_info!/trsm.
| # Metal.jl's MPS Cholesky and triangular solves work on full MtlMatrix objects, | ||
| # but panel views dispatch to host LAPACK/BLAS. Panel GEMM/SYRK are supported | ||
| # and faster; POTRF/TRSM stay on KA. Uniform batches use only KA dense kernels. | ||
| function SparseDirectSolver._allow_generic_dense(op::Symbol, ::MetalBackend, ::Type{T}) where {T} | ||
| return op === :gemm || op === :syrk | ||
| end |
There was a problem hiding this comment.
Deviation from the task text, needs to be in the Report. T31 says: "on oneAPI/Metal :auto must prefer :ka" over the allocating :generic path. With this hook select_impl(:gemm/:syrk, …, :auto) on Metal resolves to :generic (mul! on MtlArray views → MPS with per-call descriptor allocation). If the measurement justifies it, fine, but it is the opposite of what the task asks and must be written into the Report's "Deviations" with the measured difference (the plan doc has numbers), and the comment here should say that :generic allocates per call on Metal.
nit (non-blocking): the sentence "POTRF/TRSM stay on KA" is no longer accurate for POTRF since _offset_potrf_plan routes fronts ≥ 64 wide to MPS; update the comment.
| SparseDirectSolver._serial_cholesky_stats(::MetalBackend, nbatch::Integer) = nbatch > 1 | ||
| SparseDirectSolver._host_reduce_cholesky_info(::MetalBackend, nbatch::Integer) = nbatch > 1 |
There was a problem hiding this comment.
nit (non-blocking): please say why the 256-work-item _cholesky_stats_kernel! reduction is replaced by a serial kernel plus a host reduction only when nbatch > 1 on Metal. If the shared reduction produced wrong info[ns + 1] for batches on Metal, that may be a latent bug of the kernel (e.g. a missing barrier that CUDA/AMD warps hide) and deserves a found-by-agent issue rather than a backend workaround; if it was a compiler limit, name it. As written, a reader cannot tell whether the two hooks are both needed (the serial kernel already writes info[ns + 1], and _numeric_info! recomputes it on the host).
| # Integer square root without Base.isqrt, whose Julia 1.13 implementation uses | ||
| # Float64 internally and therefore cannot compile for Metal kernels. | ||
| @inline function _kernel_isqrt(x::T) where {T <: Integer} | ||
| x <= one(T) && return x | ||
| lo, hi = zero(T), min(x, T(1) << ((8 * sizeof(T) + 1) >> 1)) | ||
| while lo + one(T) < hi | ||
| mid = lo + ((hi - lo) >> 1) | ||
| if mid <= x ÷ mid | ||
| lo = mid | ||
| else | ||
| hi = mid | ||
| end | ||
| end | ||
| return lo | ||
| end |
There was a problem hiding this comment.
nit (non-blocking, but please measure on CUDA): _tri_decode sits in the innermost update loops of the regime-A and regime-B kernels on every backend (subtree.jl:181, front.jl:72, fused.jl:833/929/951). Replacing isqrt (one hardware sqrt) with a 16–32-iteration binary search that does an integer division per iteration adds a few hundred instructions per work item per tile on CUDA and AMDGPU. The two ±1 fixups that follow already make an approximate root exact, so a cheaper Metal-safe seed is unsafe_trunc(typeof(p), sqrt(Float32(8p + 1))) (exact enough for the p these kernels see: r is bounded by the 64 KiB local-memory classes, so 8p + 1 ≪ 2^24). Either way, the CUDA self-hosted job should confirm the regime-A/B timings in bench/ did not regress; the PR description does not mention this change at all.
| A = sprand(Float32, 9, 9, 0.3) + I | ||
| @test to_host(to_device(backend, A)) == A | ||
| for INT in INTTYPES | ||
| dA = to_device(backend, A, INT) | ||
| @test to_host(dA) == A | ||
| @test eltype(dA) == ComplexF64 | ||
| @test eltype(dA) == Float32 |
There was a problem hiding this comment.
Weakened test. This round trip used to check that the vendor sparse constructors on CUDA and AMDGPU preserve ComplexF64 (issue #30 territory). Switching every backend to Float32 drops the only complex device transfer check in the helpers. Make the element type per backend instead:
TC = backend_name(backend) == "Metal" ? Float32 : ComplexF64
A = sprand(TC, 9, 9, 0.3) + I
@test to_host(to_device(backend, A)) == A
for INT in INTTYPES
dA = to_device(backend, A, INT)
@test to_host(dA) == A
@test eltype(dA) == TCThe Float32 fallback for Metal is right since its to_backend rejects anything but Float32.
There was a problem hiding this comment.
VERDICT: CHANGES_REQUESTED (head 5575018)
Checked: the T31 section of TASKS.md on main, issue #118, AGENTS.md, the full diff of the four commits (Project.toml, the new Metal extension, src/dense/{capabilities,interface}.jl, src/numeric/{factorize,storage}.jl, src/solve/sweeps.jl, src/solver.jl, src/symbolic/schedule.jl, tests, docs, bench, METAL_OPTIMIZATION_PLAN.md), the surrounding code the new hooks plug into (aux allocation, ka_chol_check_info!, the pivot_epsilon semantics of the post-potrf check, front_ptr = layout.panel_ptr and the pivot handling of the fused solve kernel for regime-C fronts, _cholesky_stats_kernel! vs the serial variant, info_host sizing), git diff --check (clean), and gh pr checks (all CI legs still pending at review time; fix is the implementer workflow job, not a check). PLAN.md and Manifest.toml are untouched; public names and parameter strings are unchanged. I could not run the suite in this session (no permission to launch Julia); CI covers the CPU, CUDA and AMDGPU legs.
The Metal path itself reads correctly where I could verify it statically: MPS row-major plus lower = false yields the column-major lower factor, the post-MPS ka_chol_check_info! matches the :vendor/:generic pivot-epsilon semantics (#127), routing regime-C fronts through the fused level solve is layout-compatible (front_ptr is the panel pointer and the kernel applies piv), the serial stats kernel reproduces the parallel one field for field, _kernel_isqrt is exact, and the test-helper changes keep CPU/CUDA/AMDGPU schedules identical (_backend_schedule_options is the identity there).
Blocking findings
- No Report block, no status marker, and the PR closes a task issue it does not complete.
TASKS.mdis not touched: T31 is still[ ], there is no Report (test counts, measurements, deviations, the one-line justification for the new weak dependencyMetal, follow-ups). The body saysCloses #118, but the T31 deliverables (1) oneAPI extension and (3) allocation-free:kafallbacks withselect_implpreferring:ka(#53, #112) are not delivered, so merging would close the task issue with two thirds of the task open. AGENTS.md requires an unfinished task to stay a draft withneeds-owner, or the owner to split the task inTASKS.mdfirst (e.g. aT31a: Metal extensionsection with its own Report); the PR is also ahead of T24 to T30 in the chain, which is for the owner to record. Either way the PR needs a Report block inTASKS.mdwhose claims match the diff before it can merge. - MPS status buffer overrun (
ext/SparseDirectSolverMetalExt.jl:48-54): the status output ofMPSMatrixDecompositionCholeskyis anNSIntegerenum (8 bytes); the buffer passed isN.aux, aMtlVector{Float32}of length 1 (4 bytes), so every wide front writes past the end and clobbers theauxstatistic. Allocate a dedicated status buffer in the plan atallocate_numerictime (the prototype inbench/metal_mps_proto.jlalready does it right). - Weakened test (
test/test_helpers.jl:28-33): the complex (ComplexF64) device round trip on CUDA/AMDGPU was replaced byFloat32for every backend. Pick the type per backend so the vendor backends keep the complex check.
Must be recorded in the Report as deviations (not blocking by themselves)
:autoon Metal resolvesgemm/syrkto the allocating:genericpath, the opposite of the task text (":automust prefer:ka"); give the measurement that justifies it.- Per-front
MPSCommandBuffercreation inside refactorization (about 2.1 MB of host allocation per refactorization per the plan document) violates the "no allocation inside the numeric phase" rule; needs a follow-up issue and a Report entry ("Follow-up issues opened: none" is not accurate). ComplexF32is excluded although the task says "Float32/ComplexF32 only"; LDLT/LU on Metal are forced to regime C (31-buffer limit); the SPD regime-C thresholds are overridden on Metal. All three are reasonable, all three belong in the Report._tri_decodenow uses a division-based binary search in the innermost loops of the regime-A/B kernels on every backend; confirm on the CUDA runner that regime-A/B timings did not regress, or seed fromsqrt(Float32(...))with the existing fixups (inline nit).
Nits (non-blocking): comment why the batched Cholesky stats reduction is bypassed on Metal (latent kernel bug or compiler limit?); explain the subtree_max_fronts = 32 change to the shared NUMERIC_ABC_OPTS fixture; move METAL_OPTIMIZATION_PLAN.md out of the repository root (its Execution Record is the material for the Report); update the "POTRF/TRSM stay on KA" comment now that MPS handles POTRF for fronts at least 64 wide.
|
CI-FIX round 1 of 3: https://github.com/exanauts/SparseDirectSolver.jl/actions/runs/38506535424 failed on 5575018; Claude is diagnosing it. |
Owner-side review fixes (f0e3179) and open decisionsFixed in f0e3179
Tests on the owner's machine (affected files, all four element types): CPU 5,817 pass / 0 fail, CUDA (RTX 4080) 5,584 pass / 0 fail. AMDGPU: this CI run. Still to be decided by the owner
Not addressed (review nits)
Pipeline noteBoth automatic rounds on 🤖 Generated with Claude Code |
| function SparseDirectSolver._offset_potrf_info!(::MetalBackend, plan::MetalPotrfPlan, info, idx) | ||
| fill!(view(info, idx:idx), Int32(0)) | ||
| # the KA kernels before (assembly) and after (`ka_chol_check_info!`, trsm) run on the | ||
| # same `global_queue`, which orders them with this command buffer | ||
| cmdbuf = MPSCommandBuffer(global_queue(Metal.device())) | ||
| encode!(cmdbuf, plan.kernel, plan.panel, plan.panel, Base.unsafe_convert(MTLBuffer, plan.status)) | ||
| commit!(cmdbuf) | ||
| return true | ||
| end |
There was a problem hiding this comment.
Blocking (correctness of info on wide fronts): the MPS decomposition status is discarded, so a failed pivot inside a front ≥ 64 wide is only reported if MPS happens to leave a non-finite or non-positive entry on the diagonal.
The :vendor route (_potrf_info_impl!) writes cuSOLVER's devInfo into info[idx] and then runs ka_chol_check_info!; this route zeroes info[idx], runs MPS, and relies on the diagonal scan alone. What MPS writes into the result matrix on MPSMatrixDecompositionStatusNonPositiveDefinite is not documented as "NaN at the failing column" (LAPACK-style implementations stop at the failing column and leave the rest of the panel as the unfactored input, whose diagonal can be positive), so info may come back 0 with a garbage factor — the signal MadNLP uses for inertia regularization (#127).
Nothing in the suite covers this: the "first non-positive pivot" tests in test_numeric_cholesky_{b,c}.jl use singular_block_matrix(T, 60, 23) (banded, fronts far narrower than MPS_POTRF_MIN_WIDTH), and test_api.jl's info > 0 case is 40×40. The plan document's Phase 1 listed "factorization status behavior on an indefinite input" as a value to record; the Execution Record does not record it.
Fix, either of:
- Fold the status into
info: oneMPSMatrixDecompositionStatusbuffer per wide front (encode!takes a bareMTLBuffer, so the shared buffer cannot carry per-front values anyway), then a one-item KA kernel (or acopyto!into a view ofinfo) that setsinfo[s] = w(or 1) when the status is nonzero, beforeka_chol_check_info!refines it; or - Demonstrate on Metal that MPS leaves a detectable diagonal: add a test with an indefinite front at least 64 wide (e.g.
random_spd(T, 300, …)with one root-separator diagonal entry negated,pivot_epsilon = 0), assertfactorize!returns the referenceinfo, and record the result in the Report.
Also nit (non-blocking): the comment above _offset_potrf_status ("never read") should then say why that is safe.
| On Metal, the default SPD schedule uses narrower regime-C thresholds than the | ||
| CUDA/ROCm schedule (`32` columns and `128` rows), Metal.jl's panel GEMM/SYRK, | ||
| and fused level solves for every front. Regime-C Cholesky fronts at least 64 | ||
| columns wide use offset-aware MPS decomposition objects cached at analysis. On | ||
| an M4 Max, the 90,000-row `lap2d_300` warmed refactorization fell from 142 ms to | ||
| 62 ms while its 5.2 ms solve time was preserved. Explicit schedule options are | ||
| never replaced by this backend tuning. LDLT and LU currently use the regime-C | ||
| factorization path because their fused kernels exceed Metal's 31-buffer argument | ||
| limit. |
There was a problem hiding this comment.
nit (non-blocking), two inaccuracies in this paragraph:
- "Explicit schedule options are never replaced by this backend tuning" holds for the SPD/HPD thresholds only. For
"S","H","G"and for every uniform batch,_backend_schedule_options(::MetalBackend, …)unconditionally setsfactorization_algto the vendor path and emptiessubtree_budgets, whatever the user passed. Say so (it is a hardware limit, which is fine, but a user who setssubtree_budgetsexplicitly should learn here why it has no effect). - "offset-aware MPS decomposition objects cached at analysis" — they are built in
allocate_numeric(numeric allocation), not in the analysis.
| @inline function _kernel_isqrt(x::T) where {T <: Integer} | ||
| x <= one(T) && return x | ||
| lo, hi = zero(T), min(x, T(1) << ((8 * sizeof(T) + 1) >> 1)) | ||
| while lo + one(T) < hi | ||
| mid = lo + ((hi - lo) >> 1) | ||
| if mid <= x ÷ mid | ||
| lo = mid | ||
| else | ||
| hi = mid | ||
| end | ||
| end | ||
| return lo | ||
| end |
There was a problem hiding this comment.
nit (non-blocking): _kernel_isqrt is exact (invariant lo ≤ isqrt(x) < hi holds from the initial hi, mid ≥ 1 so the division is safe, termination is guaranteed), but it puts 16 (Int32) to 32 (Int64) integer divisions per decoded index into the innermost loops of the regime-A/B kernels on every backend. The Report defers the CUDA timing check to a follow-up; a cheaper exact variant would seed the two existing ±1 fixups from a Float32 square root (unsafe_trunc(T, sqrt(Float32(x)))), which Metal compiles and which is within ±1 of the true root for the front sizes regime A/B can hold (8p + 1 < 2^24). Either way the CUDA bench/ comparison needs to be recorded before the deviation is closed.
| # small budgets and regime-C thresholds: all three regimes on a 2-D Laplacian | ||
| const NUMERIC_ABC_OPTS = Options(subtree_budgets = [8192, 16384], regime_c_width = 16, regime_c_rows = 128, | ||
| subtree_parallelism = 0) | ||
| subtree_parallelism = 0, subtree_max_fronts = 32) |
There was a problem hiding this comment.
nit (non-blocking): subtree_max_fronts = 32 changes the regime mix of the shared A+B+C fixture on CPU, CUDA and AMDGPU too, and neither the comment nor the Report says why (presumably so that Metal's 32 KiB cap still yields regime-A subtrees on laplacian2d(40,40)). Add the reason here in one line, and if the intent is Metal-only, apply it only when the backend's max_local_bytes is below 49152 so the other backends keep the fixture they had.
There was a problem hiding this comment.
VERDICT: CHANGES_REQUESTED (head f0e3179)
Checked: the T31 section of TASKS.md on main and the Report added on this branch, AGENTS.md, the full diff of the five commits (Project.toml, ext/SparseDirectSolverMetalExt.jl, src/dense/{capabilities,interface}.jl, src/numeric/{factorize,storage}.jl, src/solve/sweeps.jl, src/solver.jl, src/symbolic/schedule.jl, the test files and helpers, docs, bench, METAL_OPTIMIZATION_PLAN.md), the owner review-fix comment and the previous verdict, the code the new hooks plug into (_iview/_mview interleaving for the host info reduction, info sizing (ns+1)*nb on device and host, takes_c_path, SolvePlan built from the device Symbolic so _dense_solve_front sees MetalBackend, the width-independent loops of the fused solve kernels, _potrf_info_impl! for the :vendor/:generic status semantics, ka_chol_check_info!, vendor_c = alg != "algo3", the Options fields the Metal override reads), git diff --check (clean), gh pr checks (all legs pending at review time). PLAN.md and Manifest.toml are untouched; TASKS.md changes only in T31; public names, parameter and phase strings unchanged; the new weak dependency Metal is justified in the Report. I could not run Julia or fetch the Metal.jl sources in this session; CI covers CPU/CUDA/AMDGPU, the Metal numbers are the hardware run of the implementer.
What is right at this head: the two blocking code findings of the previous round are fixed (the MPS status has its own MPSMatrixDecompositionStatus buffer instead of aliasing N.aux; the ComplexF64 round trip is back on CUDA/AMDGPU). The Report block exists, T31 is [~], and every deviation the previous review asked for is recorded. The serial stats kernel reproduces the 256-item reduction field by field; the host info reduction uses the interleaved layout correctly; _kernel_isqrt is exact; the regime-C override for S/H/G on Metal is consistent with vendor_c; the AMDGPU test fix (maximum(opts.subtree_budgets) > max_local_bytes) is the right predicate given resolve_subtree_budgets rejects explicit budgets above the cap.
Blocking findings
- MPS failure status is discarded (
ext/SparseDirectSolverMetalExt.jl:63-71, inline comment). The:vendorroute feeds the vendordevInfointoinfobefore the diagonal check; the MPS route zeroesinfo[s], never reads the status, and relies on MPS leaving a non-finite or non-positive diagonal on a failed pivot, which is not documented behaviour. No test exercises a failing pivot in a front >= 64 wide (theinfotests use the banded 60x60singular_block_matrixand a 40x40 API case). Either fold the per-front status intoinfo, or add a Metal test with an indefinite front >= 64 wide and record it in the Report. This is theinfocontract the MadNLP inertia correction depends on (#127). - Scope/closing is inconsistent: the Report marks T31
[~]with parts (1) oneAPI and (3) allocation-free:kafallbacks undone, while the PR body saysCloses #118and the PR is not a draft. Green CI plus an approval would let the pipeline merge, close the task issue with two thirds of the task open, and chain the next task. Per AGENTS.md either the owner splits the task inTASKS.mdonmain(e.g.T31a — Metal extension, with this Report under it andClosespointing at its issue), or the body dropsCloses #118and the PR stays open (draft,needs-owner). The owner listed this as an open decision; it has to be made before the verdict can be APPROVE. - Follow-ups without issues: the Report lists four follow-ups (reusable MPS command-buffer encoding for the per-front host allocation in the numeric phase; the serial stats kernel needed on Metal batches; the CUDA timing check of
_tri_decode;ComplexF32on Metal) and the PR body says "Follow-up issues opened: none". AGENTS.md requiresfound-by-agentissues cited in the Report for out-of-scope findings; the numeric-phase allocation in particular is an explicit rule exception that needs a tracked issue.
Non-blocking (inline nits): the docs paragraph in backends.md overstates "explicit options are never replaced" (S/H/G and batches are forced to regime C) and says the MPS objects are cached "at analysis" (numeric allocation); _kernel_isqrt puts 16-32 integer divisions per index into the regime-A/B inner loops on every backend (a Float32-seeded root with the existing fixups would be cheaper, and the CUDA bench check is still owed); subtree_max_fronts = 32 in the shared NUMERIC_ABC_OPTS fixture lacks a stated reason; METAL_OPTIMIZATION_PLAN.md still sits in the repository root.
`KernelAbstractions.GPU` is a deprecated alias of `Backend` in KA 0.10, so `backend isa KernelAbstractions.GPU` holds for the CPU backend too: under 0.10 the CPU backend took the device symbolic-map route (`device_maps_supported`, `_device_backend`), which is meant for GPUs only. Test `!(backend isa CPU)` instead, which means the same on 0.9 and 0.10. The compat keeps both versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AMDGPU budget test, Report - The MPS Cholesky status output was written into `N.aux` (the max |a_ij| statistic); the Metal extension now allocates one `MPSMatrixDecompositionStatus` buffer per numeric allocation (`_offset_potrf_status`), and the `vendor_status` plumbing is gone. - test_helpers: keep the ComplexF64 device round trip on CUDA/AMDGPU, Float32 on Metal only. - test_numeric_cholesky_a: an explicit budget above `max_local_bytes` is rejected by the analysis on every GPU backend; the predicate compared the rounded local class and failed on AMDGPU once the helpers passed max_local. - TASKS.md: T31 Report (Metal part only, status [~]). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ext/SparseDirectSolverOneAPIExt.jl (weak dependency oneAPI): CSR adapters for oneSparseMatrixCSR/CSC, `oneSparseMatrixCSR(::CSR)`, `to_backend` (Float64 only on devices that support it), the T13 API forwarders, `max_local_bytes` from the device's shared local memory, oneMKL bindings for gemm, syrk/herk, trsm, potrf/getrf (C shims, so the LAPACK info is kept), strided-batched gemm and trsm. No sytrf (oneMKL aborts with unsupported_device on the Max 1550) and no batched potrf/getrf (one status per batch, not per member). * `_auto_vendor_dense` hook (src/dense/interface.jl): every oneMKL call waits for its queue and LAPACK allocates a scratchpad, so `:auto` keeps potrf/getrf on the KA kernels on oneAPI; `impl = :vendor` still reaches them. The generic LinearAlgebra path is disabled on oneAPI. * Tests: oneAPI in test/backends.jl, the oneMKL capability column and the `:auto` policy in test_dense.jl, the extension check and the local-memory cap. * Docs: lib/oneapi.md, backend table, README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oat32) * test-gpu gains a `oneapi` leg on the self-hosted Intel GPU runner (labels gpu, oneapi): oneAPI.jl from its main branch, the only one that supports KernelAbstractions 0.10 so far. * test-metal: GitHub's hosted Apple Silicon runner, Float32 only (job-level SDS_TEST_ELTYPES), fails early when Metal is not functional there. * AGENTS.md: the oneapi and Metal legs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 1550 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f0e3179 to
2698fae
Compare
Closes #118
Task: T31 — oneAPI and Metal extensions, allocation-free KA dense fallbacks
What was built: Added a Float32 Metal.jl extension with Metal-backed CSR upload, backend capability policies, 32 KiB local-memory scheduling, fused solves, and cached offset-aware MPS Cholesky for wide fronts. Added Metal test integration, documentation, correctness-gated CPU/Metal benchmarks, and the executed optimization plan.
Tests: Full Float32 Metal suite: 23,399 pass / 0 fail / 1 existing broken. Affected Metal numerical suites after final solve/dense tuning: 5,512 pass. CPU Float32 allocation-sensitive Cholesky suites: 1,139 pass. Aqua: 9 pass. CUDA/AMDGPU: pending CI.
Deviations from PLAN.md / TASKS.md: This PR implements and hardware-tests the Metal half of T31 only. oneAPI and the broader allocation-free KA launch-configuration work remain open. Metal LDLT/LU use regime C because Apple GPUs expose 31 indirect buffer resources while the fused kernels bind 32. Metal currently supports Float32 input/factors only.
Follow-up issues opened: none.
oneAPI (added)
ext/SparseDirectSolverOneAPIExt.jl: oneMKL sparse adapters, the public API on them, oneMKL BLAS bindings (gemm, syrk/herk, trsm, strided-batched gemm/trsm) and LAPACK potrf/getrf through the C shims.:autokeeps LAPACK on the KA kernels via a new_auto_vendor_densehook, because every oneMKL call blocks the host and LAPACK allocates a scratchpad.main, the only version that supports KernelAbstractions 0.10. The KA compat stays"0.9, 0.10".backend isa KernelAbstractions.GPUwas true for the CPU backend, which sent it down the device symbolic-map route.oneapileg on the runnerheadroom(labelsgpu,oneapi), and atest-metaljob on GitHub's hostedmacos-latestrunner, Float32 only.:kafallbacks (Dense :ka/:generic paths and the KA CPU backend allocate in the numeric phase (Julia 1.10, coverage) #53) are still open, so T31 stays[~].🤖 Generated with Claude Code