Repository navigation
Add EinFold's dense algorithm to einfold-kernels - #45
Conversation
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Dense kernel contract and positional indexing crates/einfold-kernels/src/lib.rs, crates/einfold-kernels/src/dense.rs |
The crate adds Options and the dense module. The dense kernel converts eligible integer keys into positions and defines density and block-size rules. |
Dense fold calculation crates/einfold-kernels/src/dense.rs |
The kernel checks fold eligibility, builds dense storage for operand b, accumulates results in ordered blocks, and maps results to domain batches or output groups. |
Algorithm selection and randomized validation crates/einfold-kernels/src/lib.rs, crates/einfold-kernels/tests/kernels_vs_sql.rs |
fold tries the dense kernel before the hash kernel and returns the selected algorithm. Randomized tests vary thread counts and input order, compare outputs, and check dense-path coverage. |
Priority:
Estimated code review effort: 4 (Complex) | ~45 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant Caller
participant fold
participant dense as dense::fold
participant hash as hash::fold
Caller->>fold: fold with Options
fold->>dense: attempt dense execution
alt Dense kernel handles the fold
dense-->>fold: output batches
else Dense kernel declines the fold
dense-->>fold: None
fold->>hash: execute with configured Host
hash-->>fold: output batches
end
fold-->>Caller: selected Algorithm and output batches
Possibly related issues
- xqlsystems/einfold#39: This PR adds the positional dense kernel described by the issue, but does not add the provider or host integration.
- xqlsystems/xarray-sql#285: This PR implements dense fold execution, one of the workstreams described by the issue, but does not complete the wider benchmark integration.
Possibly related PRs
- xqlsystems/einfold#44: This PR adds dense execution and fallback to the hash implementation introduced in PR
#44. - xqlsystems/einfold#43: This PR consumes the fold IR types defined in PR
#43. - xqlsystems/einfold#42: This PR implements the dense execution and fallback architecture specified in PR
#42.
Merge Risk: 
Minimal · up to 5ef5e
-
No merge-blocking issue is established; the change is ready to merge after normal checks.
Security architecture risk: 
Low · up to 5ef5e
-
The change remains within an existing library boundary and does not establish a new service or privilege boundary. Eligibility checks constrain the new execution path. No material security regression was established, but downstream deployment exposure remains unknown.
Retained concerns
No architecture-level concerns identified.Security review details
-
Security Blast Radius
- inferred — The demonstrated execution scope is the embedding process and its supplied batches. Repository callers establish test and documentation usage, not a deployed network endpoint or a cross-tenant exposure boundary.
Trust Boundaries and Controls
- observed — Caller-selected host comparison rules continue to reach hash execution. Dense execution accepts only integer positional keys, so floating-point key comparison policy does not apply there. The requested thread count is normalized to at least one and capped by the number of dense tasks.
Resilience and Maintainability Implications
- observed — Scoped workers complete before successful result publication, and partial outputs remain local. A worker panic propagates through join rather than becoming a recoverable Result or selecting hash; no concrete ordinary-input worker panic was established.
-
Pre-merge checks: 
5
-
Full details: Description check
-
Explanation
The description explains what changed, why it changed, fallback behavior, determinism, and test coverage. It is mostly complete, but the required "Closes #" issue reference is empty and the testing details are not under the "How it is tested" heading.
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 12 functions across 3 files.
-
Finishing Touches
-
📝 Generate docstrings
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
2ae0efe to
3e56b09
Compare
62a056a to
c552bc5
Compare
3e56b09 to
2c7310e
Compare
606642c to
1bf016e
Compare
|
🪤 Claude (fuzz adversary) No bugs found in the dense algorithm, here or in its speedups (#53, #56), after a search aimed at where I expected them: the edges where it changes how it splits work.
Two observations, neither a bug:
|
|
|
||
| /// `out[x][y] = Σ_z a[x, z]·b[z][y]` over the rows of `a` (`at` gives each | ||
| /// position's row), skipping NULL factors of `a`, in fixed blocks. | ||
| fn accumulate( |
There was a problem hiding this comment.
Does SIMD fit here or in this file?
High-level question: how will this run on CUDA or another accelerator?
There was a problem hiding this comment.
🪭 Claude (einfold)
SIMD. It already applies here, through the compiler: accumulate's inner loop, o += v * b over two contiguous slices, autovectorizes. Two things limit it:
- The build targets baseline x86-64, which is SSE2, two doubles per instruction. AVX2 has four, and AVX-512 eight. We set no
target-cpu, and calling a function compiled for AVX2 from one that isn't needsunsafe(we forbid it), or a crate that does the runtime dispatch for us, such asmultiversionorpulp. - This kernel is mostly memory-bound. Each
o += v * breadsb's row and reads and writeso's. In the gate, the folds' time is mostly reading 800,000 rows' keys and factors and writing the output (see Add M1's gate benchmark: ddx's matmul gradient with einfold on and off #49's README). Wider vectors would help the arithmetic, not that.
So SIMD matters most for the compute-bound kernels: M2's fused attention, whose time is in the two matrix products per tile. There I'd like runtime dispatch to AVX2 + FMA, measured against what we have. One catch: FMA rounds a·b + c once, not twice. That's within the error bound and still deterministic on one machine, but the bits differ between machines with and without FMA. I haven't measured target-cpu=native yet: it rebuilds DataFusion too, a second ~10 GB build cache, so I'll do it when M2's kernel is ready.
CUDA and other accelerators. The design (§16, M8) answers it this way: einfold doesn't run on the GPU itself. It produces plans, and GPU hosts (Sirius, NVIDIA's GQE, gpudb) run them. EinFoldExec and these kernels are the CPU reference executor. They define what a host's Fold operator must compute, bit for bit, and its tests carry over as a conformance suite. What carries over to a GPU kernel:
- Dense positions: a fold with dense keys is a batched GEMM or GEMV over arrays built by a scatter, which is cuBLAS's or CUTLASS's job.
- Fixed blocks chosen from the shape: the same thing keeps a GPU kernel deterministic, as long as each output tile has one owner and adds its terms in order.
- Where it can't: a kernel that combines partial sums with atomics needs an order-independent accumulator. Design §8.6 measured a binned one at about 3.5× a plain sum on a GTX 1080 Ti.
If you'd rather have einfold drive a GPU directly, through cudarc or wgpu from EinFoldExec, that's a design change, since principle 1 is "einfold rewrites, hosts execute". It's a reasonable one for the reference executor, and I'd spike it after M2.
2c7310e to
c694fc5
Compare
1bf016e to
8f95d72
Compare
|
LGTM |
When every dimension of a two-operand fold is an output of one operand or summed and held by both, and its keys are small non-negative integers that fit in BIGINT, the fold is computed by positions: the second operand as a dense matrix, the first streamed into dense output rows, in fixed blocks chosen from the shape so the bits don't depend on the thread count. With a domain whose default is 0.0 it may ignore which groups exist; otherwise the dense operand must be complete. Anything else takes the hash path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
…nce when unused With a COALESCE default no value is NULL, so the value column is built as a plain vector. Which groups have values is computed only when the default doesn't make it irrelevant, and the no-domain path computes existence itself. On ddx's Ā at n = 50,000 this takes the operator from 12-13 ms to about 11 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
8f95d72 to
5ef5e63
Compare
…els (#46) 🪭 Claude (einfold) Fourth pull request of the restacked M1 (design v17, #42), stacked on #45. It replaces #37's and #38's DataFusion code with a thin layer over `einfold-kernels`, and fixes the operator problems the review of #37 and #38 found. **What's here** - `FoldNode`: a fold in a logical plan. Its inputs are the two operands, then the domain, and its schema is `einfold_kernels::schema`'s. - `EinFoldExec`: runs the kernels, with `HOST` (DataFusion 55's key equality, pinned by `tests/datafusion_behaviors.rs`). `EinFoldPlanner` and `EinFoldQueryPlanner` plan the node. **Behaving like a DataFusion operator** (the review's findings): - **Memory:** it reserves its inputs' memory from the session's memory pool, then the concatenated operands' and the output's, releasing the parts as they're concatenated. It fails with DataFusion's "Resources exhausted" when the pool can't give it the memory, rather than overrunning. - **Off the async runtime:** the kernels run in a blocking task (`SpawnedTask::spawn_blocking`). Before, the dense kernel's scoped threads blocked a Tokio worker. - **Metrics:** `dense_folds`, `hash_folds`, `elapsed_compute` and `output_rows`. - **Still to come:** streaming operands into the dense arrays as they arrive. The operator still holds its inputs whole, now counted against the pool. **Speed.** `examples/matmul_gradient.rs` runs ddx's two `matmul` gradients as a fold with a domain, plus the `CASE` that gives NULL where the domain's value is NULL, against the SQL ddx writes. Best of 5, 12 threads, i7-8700, DataFusion 55.2: | n | gradient | SQL | `Fold` | speedup | |---:|---|---:|---:|---:| | 50,000 | Ā = Ȳ·Wᵀ | 86–88 ms | 10.7–11.0 ms | 8× | | 50,000 | W̄ = Aᵀ·Ȳ | 27 ms | 14.3 ms | 2× | Run back to back, #38's operator took 9.6–10.0 ms and 13.2–14.8 ms on the same shapes. The extra millisecond buys the reservation, the blocking task, and the `CASE`, which is now a projection rather than part of the fold. **Tests.** The kernels' property test (#44, #45) carries the semantics. `tests/operator.rs` adds hand tests of what the operator does: - ddx's gradient through `FoldNode` plus the projection equals ddx's SQL exactly (integer values, so every sum is exact), with 1, 3 or 5 input partitions, 1, 2 or 6 threads, and batch sizes of 5, 64 and 8,192; - `EXPLAIN ANALYZE` shows the fold and `dense_folds=1`; - a 4 KiB memory pool fails cleanly. Writing that SQL turned up a parser quirk: `a IS NOT DISTINCT FROM b AND c` parses as `a IS NOT DISTINCT FROM (b AND c)`, where PostgreSQL binds `IS DISTINCT FROM` tighter than `AND`. The test parenthesizes it. I'll draft it for upstream separately. About 330 non-test lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added support for planning and running fold operations in DataFusion, including folds with an optional domain. * Fold execution combines partitioned inputs and produces results in batches, with memory tracking and execution metrics. * Added an example comparing fold-based matrix gradient calculations with equivalent SQL queries across configurable input sizes. * Fold operations are available through DataFusion’s query planner and can be inspected in query plans using einsum-style notation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
🪭 Claude (einfold)
Third pull request of the restacked M1 (design v17, #42), stacked on #44. It ports #38's dense algorithm, its review fix included, into
einfold-kernels, onto the new IR.When it runs. A two-operand fold whose every dimension is an output held only by the first operand (x), summed and held by both (z), or an output held only by the second (y). That is a matrix product
out[x, y] = Σ_z a[x, z]·b[z, y], with each role's dimensions flattened into one position. Keys must be non-negative integers that fit inBIGINT, used directly as positions. Otherwise the hash path from #44 runs.foldreports which algorithm ran.Where SQL and dense arrays differ (design §8.3):
bare held as 0, but only where no factor ofais NaN or infinite, since0·∞is NaN where SQL forms no product.+0.0,COALESCE(SUM(…), 0.0)as ddx's gradients write it, makes an unreached group and an all-NULL one look the same, so existence can be ignored. With any other default, or none,bmust be complete. A group(x, y)then exists exactly whenahas a row atx, and its sum is NULL exactly when every factor ofathere is NULL.Determinism. As in #38 (spike S24): fixed blocks chosen from the shape alone, 256 output rows per block or 4,096 positions along z, added in block order, with rows visited in position order.
Options::threadscaps the threads, and the bits don't depend on it.About 320 non-test lines.
Invariants tested (
tests/kernels_vs_sql.rs, extended). Half the cases come from the dense family, chosen by the seed. Each case now runs twice, each run on 1–8 threads and in its own arrival order, and must give the same bits. The dense path must run in at least a quarter of the dense family; it runs in about 35%. The dense family leans to the0.0default ddx uses.🤖 Generated with Claude Code
https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS
Summary by CodeRabbit