Skip to main content

Pass Pipelines

This guide covers creating, configuring, and optimizing pass pipelines in HETorch.

What is a Pass Pipeline?

A PassPipeline is an ordered sequence of transformation passes that transforms a PyTorch model into an HE-ready computation graph.

from hetorch.passes import PassPipeline
from hetorch.passes.builtin import (
InputPackingPass,
NonlinearToPolynomialPass,
RescalingInsertionPass,
)

pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
RescalingInsertionPass(),
])

Creating Pipelines

Basic Pipeline

from hetorch.passes import PassPipeline
from hetorch.passes.builtin import InputPackingPass, DeadCodeEliminationPass

pipeline = PassPipeline([
InputPackingPass(strategy="row_major"),
DeadCodeEliminationPass(),
])

Running a Pipeline

from hetorch import HETorchCompiler

compiler = HETorchCompiler(context, pipeline)
compiled_model = compiler.compile(model, example_input)

Common Pipeline Patterns

1. Minimal Pipeline (Fast Compilation)

For quick testing and development:

pipeline = PassPipeline([
InputPackingPass(),
DeadCodeEliminationPass(),
])

Use when:

  • Testing model traceability
  • Rapid prototyping
  • No non-linear activations

2. Standard CKKS Pipeline

For neural networks with CKKS:

pipeline = PassPipeline([
InputPackingPass(strategy="row_major"),
NonlinearToPolynomialPass(degree=8),
RescalingInsertionPass(strategy="lazy"),
RelinearizationInsertionPass(strategy="lazy"),
DeadCodeEliminationPass(),
])

Use when:

  • Compiling neural networks
  • Using CKKS scheme
  • Moderate depth (2-4 multiplications)

3. Optimized Pipeline

With BSGS optimization for large matrices:

pipeline = PassPipeline([
InputPackingPass(strategy="row_major"),
NonlinearToPolynomialPass(degree=8),
LinearLayerBSGSPass(min_size=16),
RescalingInsertionPass(strategy="lazy"),
RelinearizationInsertionPass(strategy="lazy"),
DeadCodeEliminationPass(),
])

Use when:

  • Large linear layers (dimension ≥ 16)
  • Performance is critical
  • Willing to accept more complex graph

4. Deep Network Pipeline

With bootstrapping for deep computations:

pipeline = PassPipeline([
InputPackingPass(strategy="row_major"),
NonlinearToPolynomialPass(degree=8),
LinearLayerBSGSPass(min_size=16),
RescalingInsertionPass(strategy="lazy"),
RelinearizationInsertionPass(strategy="lazy"),
BootstrappingInsertionPass(level_threshold=15.0, strategy="greedy"),
DeadCodeEliminationPass(),
])

Use when:

  • Deep networks (>4 layers)
  • High multiplication depth
  • Need to refresh noise budget

5. Debug Pipeline

With visualization and analysis:

pipeline = PassPipeline([
GraphVisualizationPass(prefix="01_original"),
InputPackingPass(strategy="row_major"),
GraphVisualizationPass(prefix="02_packed"),
NonlinearToPolynomialPass(degree=8),
GraphVisualizationPass(prefix="03_polynomial"),
RescalingInsertionPass(strategy="lazy"),
GraphVisualizationPass(prefix="04_rescaled"),
DeadCodeEliminationPass(),
PrintGraphPass(verbose=True),
CostAnalysisPass(verbose=True),
])

Use when:

  • Debugging compilation issues
  • Understanding transformations
  • Analyzing performance

Pass Dependencies

Understanding Dependencies

Passes declare dependencies using requires and provides:

class MyPass(TransformationPass):
requires = ["input_packed"] # Needs InputPackingPass to run first
provides = ["my_property"] # Guarantees this property after running

Dependency Chain

InputPackingPass
↓ provides: input_packed
LinearLayerBSGSPass (requires: input_packed)
↓ provides: linear_bsgs
RescalingInsertionPass
↓ provides: rescaling_inserted
BootstrappingInsertionPass (requires: rescaling_inserted)
↓ provides: bootstrapping_inserted

Validation

Pipeline validates dependencies before running:

# This will fail: BootstrappingInsertionPass requires rescaling_inserted
pipeline = PassPipeline([
InputPackingPass(),
BootstrappingInsertionPass(), # ERROR: requires rescaling_inserted
])

# This works: RescalingInsertionPass provides rescaling_inserted
pipeline = PassPipeline([
InputPackingPass(),
RescalingInsertionPass(),
BootstrappingInsertionPass(), # OK: dependency satisfied
])

Pipeline Configuration

Pass-Specific Configuration

Each pass can be configured independently:

pipeline = PassPipeline([
InputPackingPass(
strategy="row_major",
slot_count=4096
),
NonlinearToPolynomialPass(
degree=10, # Higher degree for better approximation
functions=["relu", "gelu"], # Only replace these
approximation_method="chebyshev"
),
RescalingInsertionPass(
strategy="lazy" # Fewer rescales
),
])

Strategy Selection

Rescaling Strategy

# Eager: More rescales, simpler
RescalingInsertionPass(strategy="eager")

# Lazy: Fewer rescales, more efficient
RescalingInsertionPass(strategy="lazy")

Relinearization Strategy

# Eager: Relinearize after every cmult
RelinearizationInsertionPass(strategy="eager")

# Lazy: Only when necessary (recommended)
RelinearizationInsertionPass(strategy="lazy")

Bootstrapping Strategy

# Greedy: Bootstrap as soon as threshold reached
BootstrappingInsertionPass(strategy="greedy", level_threshold=15.0)

# Optimal: Find optimal placement (future)
BootstrappingInsertionPass(strategy="optimal", level_threshold=15.0)

Optimization Strategies

1. Reduce Polynomial Degree

Lower degree = fewer multiplications:

# High accuracy, slow
NonlinearToPolynomialPass(degree=10)

# Balanced (recommended)
NonlinearToPolynomialPass(degree=8)

# Fast, lower accuracy
NonlinearToPolynomialPass(degree=6)

2. Use Lazy Strategies

Lazy strategies reduce unnecessary operations:

pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
RescalingInsertionPass(strategy="lazy"), # Fewer rescales
RelinearizationInsertionPass(strategy="lazy"), # Fewer relinearizations
DeadCodeEliminationPass(),
])

3. Enable BSGS for Large Matrices

BSGS reduces rotations for large matrices:

pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
LinearLayerBSGSPass(min_size=32), # Only for matrices ≥ 32
# ... other passes
])

4. Tune Bootstrapping Threshold

Higher threshold = more bootstraps, safer:

# Conservative: Bootstrap early
BootstrappingInsertionPass(level_threshold=20.0)

# Balanced (recommended)
BootstrappingInsertionPass(level_threshold=15.0)

# Aggressive: Bootstrap late
BootstrappingInsertionPass(level_threshold=10.0)

Scheme-Specific Pipelines

CKKS Pipeline

from hetorch import HEScheme, CKKSParameters

context = CompilationContext(
scheme=HEScheme.CKKS,
params=CKKSParameters(...),
backend=FakeBackend()
)

pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
RescalingInsertionPass(strategy="lazy"), # CKKS-specific
RelinearizationInsertionPass(strategy="lazy"),
BootstrappingInsertionPass(level_threshold=15.0),
DeadCodeEliminationPass(),
])

BFV/BGV Pipeline

from hetorch import HEScheme, BFVParameters

context = CompilationContext(
scheme=HEScheme.BFV,
params=BFVParameters(...),
backend=FakeBackend()
)

pipeline = PassPipeline([
InputPackingPass(),
# No NonlinearToPolynomialPass (exact arithmetic)
# No RescalingInsertionPass (not needed for BFV)
RelinearizationInsertionPass(strategy="lazy"),
BootstrappingInsertionPass(level_threshold=20.0),
DeadCodeEliminationPass(),
])

Performance Considerations

Compilation Time

Passes affect compilation time:

PassCompilation TimeImpact
InputPackingPassFastMinimal
NonlinearToPolynomialPassFastMinimal
LinearLayerBSGSPassMediumModerate (graph expansion)
RescalingInsertionPassFastMinimal
RelinearizationInsertionPassFastMinimal
BootstrappingInsertionPassMediumModerate (iterative)
DeadCodeEliminationPassFastMinimal
CostAnalysisPassMediumModerate (graph traversal)
GraphVisualizationPassSlowHigh (SVG generation)

Execution Time

Passes affect execution time:

PassExecution ImpactWhy
NonlinearToPolynomialPassHighAdds many multiplications
LinearLayerBSGSPassPositiveReduces rotations
RescalingInsertionPass (lazy)PositiveFewer rescales
RelinearizationInsertionPass (lazy)PositiveFewer relinearizations
BootstrappingInsertionPassHighBootstrapping is expensive
DeadCodeEliminationPassPositiveRemoves unused operations

Debugging Pipelines

1. Print Graph at Each Stage

pipeline = PassPipeline([
PrintGraphPass(), # Original
InputPackingPass(),
PrintGraphPass(), # After packing
NonlinearToPolynomialPass(),
PrintGraphPass(), # After polynomial
])

2. Visualize Transformations

pipeline = PassPipeline([
GraphVisualizationPass(prefix="01_original"),
InputPackingPass(),
GraphVisualizationPass(prefix="02_packed"),
NonlinearToPolynomialPass(),
GraphVisualizationPass(prefix="03_polynomial"),
])

3. Analyze Costs

pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
CostAnalysisPass(verbose=True), # Before optimization
RescalingInsertionPass(strategy="lazy"),
CostAnalysisPass(verbose=True), # After optimization
])

4. Check Dependencies

for pass_instance in pipeline.passes:
print(f"{pass_instance.name}:")
print(f" Requires: {pass_instance.requires}")
print(f" Provides: {pass_instance.provides}")
print(f" Scheme-specific: {pass_instance.scheme_specific}")

Best Practices

1. Start Simple, Add Incrementally

# Start
pipeline = PassPipeline([
InputPackingPass(),
DeadCodeEliminationPass(),
])

# Add polynomial approximation
pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
DeadCodeEliminationPass(),
])

# Add CKKS-specific passes
pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
RescalingInsertionPass(strategy="lazy"),
DeadCodeEliminationPass(),
])

2. Use Lazy Strategies

Lazy strategies are almost always better:

# Recommended
RescalingInsertionPass(strategy="lazy")
RelinearizationInsertionPass(strategy="lazy")

# Only use eager for debugging
RescalingInsertionPass(strategy="eager")

3. Clean Up with DeadCodeElimination

Always include as one of the last passes:

pipeline = PassPipeline([
# ... transformation passes
DeadCodeEliminationPass(), # Clean up
CostAnalysisPass(), # Analyze final graph
])

4. Profile Before Optimizing

Use CostAnalysisPass to identify bottlenecks:

pipeline = PassPipeline([
InputPackingPass(),
NonlinearToPolynomialPass(),
RescalingInsertionPass(strategy="lazy"),
DeadCodeEliminationPass(),
CostAnalysisPass(verbose=True), # Identify bottlenecks
])

5. Test with Fake Backend First

Develop pipelines with fake backend, then switch to real:

# Development
context = CompilationContext(
scheme=HEScheme.CKKS,
params=CKKSParameters(...),
backend=FakeBackend(simulate_noise=True)
)

# Production (future)
context = CompilationContext(
scheme=HEScheme.CKKS,
params=CKKSParameters(...),
backend=SEALBackend(...)
)

Troubleshooting

PassValidationError

Error: PassValidationError: Pass X requires Y

Solution: Ensure required passes run before dependent passes:

# Wrong
pipeline = PassPipeline([
LinearLayerBSGSPass(), # Requires input_packed
InputPackingPass(), # Provides input_packed (too late!)
])

# Correct
pipeline = PassPipeline([
InputPackingPass(), # Provides input_packed
LinearLayerBSGSPass(), # Requires input_packed (OK)
])

SchemeValidationError

Error: SchemeValidationError: Pass X only works with scheme Y

Solution: Remove scheme-specific passes or change scheme:

# Wrong: RescalingInsertionPass with BFV
context = CompilationContext(scheme=HEScheme.BFV, ...)
pipeline = PassPipeline([
InputPackingPass(),
RescalingInsertionPass(), # ERROR: CKKS only
])

# Correct: Use CKKS
context = CompilationContext(scheme=HEScheme.CKKS, ...)
pipeline = PassPipeline([
InputPackingPass(),
RescalingInsertionPass(), # OK
])

Slow Compilation

Symptom: Compilation takes too long

Solutions:

  1. Remove GraphVisualizationPass (slow)
  2. Reduce polynomial degree
  3. Increase LinearLayerBSGSPass min_size
  4. Remove unnecessary passes

Large Output Differences

Symptom: Compiled model output differs from original

Solutions:

  1. Increase polynomial degree
  2. Adjust approximation ranges
  3. Check noise budget (enable simulation)
  4. Verify pass configuration

Next Steps