Skip to content

perf(chain): backward pass and output_min_hops optimizations#885

Merged
lmeyerov merged 91 commits into
masterfrom
refactor/df-executor-traversal-primitives
Jan 9, 2026
Merged

perf(chain): backward pass and output_min_hops optimizations#885
lmeyerov merged 91 commits into
masterfrom
refactor/df-executor-traversal-primitives

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Performance optimizations for chain.py backward pass
  • Fix output_min_hops/output_max_hops semantics to match oracle
  • Add 38+ tests for optimization coverage

Changes

Performance

  • Skip hop() in backward pass for simple single-hop edges (19-28% speedup)
  • Skip edge re-run in combine_steps for single-hop chains (14-22% speedup)
  • Extract BFS helpers to reduce duplication in df_executor

Bug Fix

  • output_min_hops now correctly filters edges by hop number, includes all their endpoints
  • Seeds excluded when output_min_hops set (they're not on output edges)
  • Tests updated to match oracle expectations

Tests

  • 38 new tests for backward pass and combine_steps optimizations
  • Feature parity tests for df_executor vs chain
  • Output slicing tests with correct oracle semantics

Related

Test Plan

  • python -m pytest tests/gfql/ref/test_chain_optimizations.py - 81 passed
  • python -m pytest tests/gfql/ref/test_enumerator_parity.py - 22 passed
  • python -m pytest tests/gfql/ - 359 passed

🤖 Generated with Claude Code

@lmeyerov lmeyerov changed the base branch from feat/issue-837-cudf-hop-executor to master January 8, 2026 01:13
@lmeyerov lmeyerov force-pushed the refactor/df-executor-traversal-primitives branch from 3a369dd to 7e15c93 Compare January 8, 2026 01:56
@lmeyerov lmeyerov added the gpu-ci label Jan 9, 2026
lmeyerov and others added 27 commits January 9, 2026 12:21
Make it explicit that the O(n!) oracle path is for testing only
and should never be called from production code paths.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When the start node is filtered, hop labels accurately identify path positions.
When unfiltered, all edges become "hop 1" from some start, making labels useless.

Fix: In _filter_multihop_by_where, check if start node is filtered:
- Filtered: use hop labels to identify start/end nodes (original behavior)
- Unfiltered: use alias frames directly as fallback

Also renamed _run_oracle to _run_test_only_oracle to prevent accidental
production usage of the O(n!) reference implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…iants

Based on 5-whys analysis of the unfiltered start bug, add systematic coverage
for the interaction matrix of: start filter × edge direction × WHERE clause.

New tests:
- test_unfiltered_start_multihop_reverse
- test_unfiltered_start_multihop_undirected
- test_filtered_start_multihop_reverse_where
- test_filtered_start_multihop_undirected_where

All tests pass, confirming the fix handles all direction variants correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add two local helper functions to df_executor.py:
- _build_edge_pairs: normalize edges for BFS traversal based on direction
- _bfs_reachability: vectorized BFS with hop distance tracking

These helpers consolidate duplicated BFS logic in:
- _filter_multihop_edges_by_endpoints (-55 lines)
- _find_multihop_start_nodes (-22 lines)

Net result: -35 lines in df_executor.py with no new files.
All 275 GFQL tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Optimize combine_steps() to avoid re-running forward ops for single-hop
edge chains. Instead of calling op() again (which does a full hop
traversal), filter edges by valid src/dst endpoints from the backward-
validated node sets.

Key changes:
- Use vectorized merge instead of set + isin for large graph performance
- For multi-hop edges, fall back to the original re-run approach

Performance improvement for single-hop chains:
- medium (10K nodes): 145ms → 41ms (72% faster)
- large (100K nodes): 207ms → 126ms (39% faster)

Chain is now faster than df_executor for large graphs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Verify that df_executor (with WHERE) produces same features as chain:
- Named alias boolean tags
- Hop labels (label_edge_hops)
- Output slicing (output_min_hops/output_max_hops)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
For single-hop edges without hop labels, use vectorized merge filtering
instead of calling op.reverse()() which triggers a full hop() traversal.
This saves ~50% of backward pass time.

Performance improvement across all graph sizes:
- tiny (100 nodes): 31ms → 25ms (19% faster)
- small (1K nodes): 33ms → 26ms (21% faster)
- medium (10K nodes): 40ms → 33ms (18% faster)
- large (100K nodes): 151ms → 109ms (28% faster)

Chain is now competitive with df_executor on medium graphs and wins
on large graphs.

The optimization:
1. Detects simple single-hop edges (min=1, max=1, no hop labels)
2. Filters edges by valid endpoints from wavefront sets
3. Computes result nodes as the backward traversal targets

Falls back to full hop() traversal for multi-hop or labeled edges.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ations

Comprehensive test coverage for chain.py optimizations:

TestBackwardPassOptimization (15 tests):
- TestOptimizationEligibility: Verify _is_simple_single_hop correctly identifies
  eligible edges (single-hop, no labels) vs ineligible (multi-hop, labeled)
- TestDirectionSemantics: Verify forward/reverse/undirected return correct nodes
- TestEdgeCases: Empty results, disconnected components, self-loops, parallel edges
- TestResultCorrectness: Tags and attributes preserved correctly

TestCombineStepsOptimization (7 tests):
- Single-hop endpoint filtering for all directions
- Hop label preservation

TestChainDFExecutorParity (6 tests):
- Same nodes/edges with and without WHERE
- Complex patterns (diamond, filtered mid-node)
- WHERE clause filtering

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…h oracle

- output_min_hops filters edges by hop number, includes all their endpoints
- Seeds (hop=0 or NA) excluded when output_min_hops set (not on output edges)
- Edge endpoint coverage ensures valid graph output
- Updated tests to match oracle expectations:
  - test_output_min_hops_filters_early_hops: expects {b,c,d}
  - test_output_max_hops_filters_late_hops: expects {a,b,c}
  - test_output_slice_both_bounds: expects {b,c}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reuse existing _is_simple_single_hop helper instead of inline check
that incorrectly detected multi-hop when hops=None from JSON.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- to_fixed_point=True means unbounded traversal, not single-hop
- Added test for to_fixed_point eligibility check

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
to_fixed_point is a direct attribute of ASTEdge, no need for getattr.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add type check to ensure where field is a list before casting,
preventing confusing runtime errors from malformed JSON.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add type checks for:
- payload is a dict
- payload has 'left' and 'right' keys
- left/right values are strings

Prevents confusing KeyError/TypeError from malformed JSON.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move sequence type check from chain.py into parse_where_json.
Remove redundant check and unsafe cast from chain.py.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move from standalone function in chain.py to method on ASTEdge class.
This centralizes edge-related logic with the edge class.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract common edge filtering logic into _filter_edges_by_endpoint()
- Simplify directed edge filtering (forward/reverse) using helper
- Condense output_min/max_hops filter block
- Tighten fast backward pass code

Reduces diff by ~20% while maintaining functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Condense prev/next node lookups to single lines
- Simplify apply_output_slice function
- Tighten multihop recompute loop
- Simplify backward pass fallback
- Extract prev_set/target_set upfront in fast backward

Reduces insertions from 307 to 166.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove WHERE clause and df_executor functionality from this PR to
focus on chain optimizations only. WHERE functionality will be
added in a stacked PR.

Changes:
- Remove same_path_types, same_path_plan, df_executor modules
- Remove Chain.where field and WHERE imports from chain.py
- Simplify gfql_unified.py by removing _chain_dispatch helper
- Remove WHERE-related test files and test classes
- Keep all chain optimization tests (78 tests passing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix E127 continuation line over-indented
- Fix W504 line break after binary operator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add assertions to narrow types in fast backward pass:
- assert isinstance(op, ASTEdge) for direction access
- assert node_id/src_col/dst_col not None for filter helper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…/compare

The same_path_types module was removed in the PR split. Import col and
compare from the enumerator module which has equivalent definitions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove WHERE/df_executor entries (moving to stacked PR 886)
- Add Performance section for backward pass optimization
- Add test entry for 78 chain optimization tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the refactor/df-executor-traversal-primitives branch from b1b115c to c14d079 Compare January 9, 2026 20:21
@lmeyerov lmeyerov merged commit 1e24a2d into master Jan 9, 2026
28 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant