Skip to content

DDIR half join: consolidate each released window within itself - #1029

Open
frankmcsherry wants to merge 1 commit into
TimelyDataflow:master-nextfrom
frankmcsherry:ddir-halfjoin-consolidate
Open

frankmcsherry wants to merge 1 commit into
TimelyDataflow:master-nextfrom
frankmcsherry:ddir-halfjoin-consolidate

Conversation

@frankmcsherry

Copy link
Copy Markdown
Member

A profile of LDBC's IC reads found the half join cheaper per row than the binary join (44 against 55 ns), but producing 2.3× more physical rows on ic1's friend hops, and 1.9× on ic13's loop join.

The cause is the batcher's fast path. HalfJoinBatcher releases arrivals the arrangement's frontier already allows without staging them, and for a one-off reading an import that is every arrival. Part::runs then sorted each released window by identifier only, so a stream that repeats a row, as converging paths do, met the arrangement once per copy and handed every copy downstream. The binary join's arrangement of its stream side merges them first.

Each released window is now sorted by (identifier, key, value, time), with its equal rows merged and rows that cancel dropped.

  • Exact: the half join is linear in its stream.
  • Real keys: the presented key leads with the identifier, so runs stay in identifier order. It also holds the real key, so keys that share a hashed identifier never merge.
  • Streaming is unchanged: only rows already released together merge. Nothing waits longer, and nothing goes through the ladder. Aggregating across releases stays a choice for the author, e.g. a binary join or an arrange before the hop.
  • One routine: TimeChunk::form, on the ladder path, uses the same consolidation.

Measurement

A three-hop friend-of-friend expansion as a one-off over an imported graph where paths converge (1,000 nodes, 50 friends each). One worker, release, three alternated runs of 30 reads each, with the probe kept out of the tree:

hop 3 physical rows ms per read
binary joins 44,734 2.72–2.78
half join, before 115,941 5.30–5.46
half join, after 44,734 2.99–3.15

Here hop 3's whole input arrives in one release, so all of the excess goes. ic13's in-loop excess repeats across rounds, at different iteration times, so some of it may span releases. That needs the LDBC harness to confirm.

Tests

  • New test: a_release_is_consolidated_within_itself checks that equal rows within one release merge, that cancelling rows vanish, and that nothing unblocked is held back. A later repeat comes out in its own extraction.
  • Crate tests: cargo test -p interactive -p ddir-server, 212 passed; dogs3's tests pass.
  • Difftest: 2,000 seeds of delta,split,bind,reinstall with late installs, and 1,000 with all oracles: all passed.
  • LDBC: the authored check (fixture, 4 workers), 41 of 41.
  • Warnings: none from cargo check --all-targets, and clippy shows nothing new.

#1027's count stage uses the same batcher, so its stream is consolidated within each window too.

🤖 Generated with Claude Code

https://claude.ai/code/session_019XsakwFGwtESwZ1gSYwG4o

The half join's batcher releases arrivals the arrangement's frontier already
allows without staging them, which for a one-off reading an import is every
arrival. `Part::runs` sorted each released window by identifier only, so a
stream that repeats a row, as converging paths do, met the arrangement once
per copy and handed every copy downstream; the binary join's arrangement of
its stream side merges them first.

Each window is now sorted by (identifier, key, value, time) and its equal rows
merged, dropping rows that cancel. The presented key leads with the
identifier, so runs stay in identifier order, and it holds the real key, so
keys sharing an identifier never merge. Only rows already released together
merge: nothing waits longer and nothing is routed through the ladder. The
half join is linear in its stream, so this is exact. `TimeChunk::form` uses
the same routine.

On a three-hop friend-of-friend expansion over a graph where paths converge
(1,000 nodes, 50 friends each, read as a one-off), the third hop's physical
rows went from 115,941 to 44,734 per read, the binary joins' count, and a
read from 5.3-5.5 ms to 3.0-3.2 ms, against 2.7-2.8 ms with binary joins.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019XsakwFGwtESwZ1gSYwG4o
@frankmcsherry

Copy link
Copy Markdown
Member Author

Checked on the LDBC Interactive v2 harness (#1010): SF1, 4 workers, M4. Each read is a one-off, installed, answered and dropped. Answers are identical to the binary-join plans in every case.

binary joins half join, master-next half join, this PR
ic1, ms per read (235 reads × 4) 11.3 22.6 17.1
ic1, friend hops only, ms per read 4.4 7.9 6.4
ic1 hop 3, physical rows (20 reads) 3.87M 9.11M 6.50M
ic13, ms per read (320 reads × 4) 9.5 12.8 12.9
ic13 loop join, physical rows (20 reads) 3.50M 6.69M 6.69M
  • ic1: about half the excess goes. The rest is hop 2's repeated rows arriving at hop 3 in different releases, probably as separate deliveries from the 4 workers. This PR doesn't merge across releases.
  • ic13: unchanged. Its repeats come in different loop iterations, so they never share a release. That's the case the description left open.
  • All 14 complex reads, with every join against an import written as a half join: 21.0 s → 19.8 s in total, against 17.3 s with binary joins. For one-off reads the binary join still wins everywhere except ic12, which is about 12% faster as half joins.

Built from this PR's head (f5b6993) and compared with master-next 7eb7da8.

🤖 Generated with Claude Code

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