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:
| Pass | Compilation Time | Impact |
|---|---|---|
| InputPackingPass | Fast | Minimal |
| NonlinearToPolynomialPass | Fast | Minimal |
| LinearLayerBSGSPass | Medium | Moderate (graph expansion) |
| RescalingInsertionPass | Fast | Minimal |
| RelinearizationInsertionPass | Fast | Minimal |
| BootstrappingInsertionPass | Medium | Moderate (iterative) |
| DeadCodeEliminationPass | Fast | Minimal |
| CostAnalysisPass | Medium | Moderate (graph traversal) |
| GraphVisualizationPass | Slow | High (SVG generation) |
Execution Time
Passes affect execution time:
| Pass | Execution Impact | Why |
|---|---|---|
| NonlinearToPolynomialPass | High | Adds many multiplications |
| LinearLayerBSGSPass | Positive | Reduces rotations |
| RescalingInsertionPass (lazy) | Positive | Fewer rescales |
| RelinearizationInsertionPass (lazy) | Positive | Fewer relinearizations |
| BootstrappingInsertionPass | High | Bootstrapping is expensive |
| DeadCodeEliminationPass | Positive | Removes 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:
- Remove GraphVisualizationPass (slow)
- Reduce polynomial degree
- Increase LinearLayerBSGSPass min_size
- Remove unnecessary passes
Large Output Differences
Symptom: Compiled model output differs from original
Solutions:
- Increase polynomial degree
- Adjust approximation ranges
- Check noise budget (enable simulation)
- Verify pass configuration
Next Steps
- Builtin Passes: Detailed pass documentation
- Backends: Choose and configure backends
- Examples: Complete working examples
- Custom Passes: Write your own passes