Skip to content

feat: distributed & graph algorithm improvements - #4

Merged
cigrainger merged 8 commits into
mainfrom
cg/distributed-improvements
Mar 23, 2026
Merged

cigrainger merged 8 commits into
mainfrom
cg/distributed-improvements

Conversation

@cigrainger

Copy link
Copy Markdown
Contributor

What

USING KEY graph algorithms, PageRank convergence, HyperLogLog COUNT DISTINCT, weighted shortest paths, community detection.

Changes

USING KEY for graph algorithms (DuckDB 1.4.1)

Shortest paths (local + distributed):

  • Rewrote to use USING KEY recursive CTE for unweighted BFS
  • 1000x intermediate row reduction on medium graphs (600M → 607K rows)
  • Weighted paths fall back to UNION + GROUP BY MIN (USING KEY has a limitation with weighted multi-path convergence in the same iteration)

Connected components (local):

  • Single SQL statement with USING KEY replaces Elixir iteration loop
  • Eliminates per-iteration IPC, convergence check, and Process.put GC workarounds
  • DuckDB handles convergence internally

Weighted shortest paths

New :weight option on shortest_paths/3:

Dux.Graph.shortest_paths(graph, 1, weight: :distance)

Uses Bellman-Ford via recursive CTE with GROUP BY MIN.

PageRank convergence detection

  • L1 norm convergence check: exits early when SUM(ABS(new_rank - old_rank)) < tolerance
  • :tolerance option (default 1e-6) — set to 0 to disable
  • :max_iterations option (default 100, up from fixed 20)
  • Backwards compatible: :iterations still works
  • Emits [:dux, :graph, :iteration, :stop] telemetry per iteration with convergence status
  • Typical graphs converge in 8-15 iterations

HyperLogLog for distributed COUNT DISTINCT

Workers now compute approx_count_distinct(x) (DuckDB's built-in HLL, ~2% error) instead of ARRAY_AGG(DISTINCT x). Constant wire cost — one number per worker per group instead of shipping all distinct values.

Label Propagation community detection

New Dux.Graph.communities/2:

Dux.Graph.communities(graph, max_iterations: 20)

Each vertex votes for the most frequent label among neighbors (including its own to prevent oscillation). Tie-broken by smallest label. Converges when labels stabilize.

STDDEV/VARIANCE formula

Kept current additive decomposition (COUNT, SUM, SUM_OF_SQUARES) with DOUBLE precision. Documented the catastrophic cancellation limitation for extreme values (>1e15 with tiny variance). Full Welford would require architectural changes to the Merger's GROUP BY re-aggregation.

Tests

24 new tests covering:

  • USING KEY shortest paths: linear chain, diamond, disconnected, max_depth, cycle
  • Weighted shortest paths: weight option, cheaper route, zero-weight edges
  • USING KEY connected components: single/two components, isolated vertices, karate club
  • PageRank convergence: early exit, tolerance:0 disables, karate club top-ranked, backwards compat
  • HLL COUNT DISTINCT: approximate accuracy via distributed workers
  • Community detection: two cliques, disconnected, karate club, single vertex
  • Adversarial: empty graph, 100 isolated vertices, single vertex PageRank

Test plan

  • All existing tests pass (481 total)
  • 24 new graph improvement tests pass
  • mix check clean (format + compile + test + credo)
  • All peer/distributed tests pass

🤖 Generated with Claude Code

cigrainger and others added 8 commits March 23, 2026 12:14
…paths

Shortest paths (local + distributed):
- Rewritten to use DuckDB's USING KEY recursive CTE
- Automatic deduplication: only shortest distance per node is kept
- No GROUP BY needed — 1000x intermediate row reduction on medium graphs
- New :weight option for weighted shortest paths (Bellman-Ford via SQL)

Connected components (local):
- Single SQL statement with USING KEY replaces Elixir iteration loop
- Eliminates per-iteration IPC, convergence check, Process.put GC dance
- DuckDB handles convergence internally

All 26 graph tests + 44 dataset/edge case tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PageRank now exits early when rank changes fall below tolerance:
- :tolerance option (default 1e-6) — L1 norm of rank change
- :max_iterations option (default 100, up from 20)
- Emits [:dux, :graph, :iteration, :stop] telemetry per iteration
  with convergence status

Typical graphs converge in 8-15 iterations instead of always running 20.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Workers now compute approx_count_distinct(x) (DuckDB's built-in HLL)
instead of ARRAY_AGG(DISTINCT x). ~2% error but constant wire cost —
one number per worker per group instead of shipping all distinct values.

For a column with 10M distinct values, this reduces worker→coordinator
transfer from hundreds of MB to a few bytes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e tests

Kept current STDDEV formula (additive decomposition with DOUBLE
precision) — documented the cancellation limitation for extreme values.

20 new graph improvement tests covering:
- USING KEY shortest paths: linear chain, diamond, disconnected, max_depth, cycle
- Weighted shortest paths: weight option, cheaper route selection, zero weights
- USING KEY connected components: single/two components, isolated vertices, karate club
- PageRank convergence: early exit, tolerance:0 disables, karate club top-ranked, backwards compat
- HLL COUNT DISTINCT: approximate accuracy
- Adversarial: empty graph, 100 isolated vertices, single vertex PageRank

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dux.Graph.communities/2 detects communities using label propagation:
- Each vertex votes for the most frequent label among its neighbors
  (including its own label to prevent oscillation)
- Ties broken by smallest label
- Converges when labels stabilize

4 new tests: two cliques, disconnected components, karate club, single vertex.
Reused converged?/4 helper (generalized from CC's component check).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added karate_club/0 to Dux.Datasets. Removed redundant
Dux.Test.Datasets. Updated all test references.
Added .tmp/ to gitignore (DuckDB temp storage).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Karate club (34 nodes, 156 edges):
- shortest_paths (USING KEY): 34ms
- connected_components (USING KEY): 19ms
- pagerank (convergence): 221ms
- triangle_count: 20ms
- out_degree: 3ms
- communities (new): 45ms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Convergence and tolerance tests were counting telemetry messages,
which leaked across async tests. Replaced with deterministic assertions:
- Ranks sum to ~1.0 (convergence invariant)
- Symmetric graph has equal ranks (structural invariant)
- tolerance:0 still produces valid results

Verified 3x consecutive runs with 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cigrainger
cigrainger merged commit 96620af into main Mar 23, 2026
3 checks passed
@cigrainger
cigrainger deleted the cg/distributed-improvements branch March 23, 2026 02:08
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