IterOps API¶
IterOps provides control flow operations: loops, convergence detection, and iteration strategies. These enable iterative graph algorithms like PageRank, label propagation, and belief propagation.
Overview¶
Access IterOps through sG.builder.iter:
@algorithm
def example(sG, max_iter=100):
values = sG.nodes(1.0)
with sG.builder.iter.loop(max_iter):
# Iterative updates
values = sG.builder.var("values", values * 2.0)
return values
Basic Loops¶
loop(count)¶
Fixed iteration loop.
with sG.builder.iter.loop(100):
# Code here runs 100 times
values = sG.builder.var("values", updated_values)
Parameters:
- count: Number of iterations (int)
Returns: Context manager
Usage:
@algorithm
def pagerank(sG, max_iter=100, damping=0.85):
ranks = sG.nodes(1.0 / sG.N)
deg = ranks.degrees()
with sG.builder.iter.loop(max_iter):
contrib = ranks / (deg + 1e-9)
neighbor_sum = sG @ contrib
ranks = sG.builder.var("ranks",
damping * neighbor_sum + (1 - damping) / sG.N
)
return ranks.normalize()
Key points:
- Updates must use sG.builder.var() to create loop-carried dependencies
- Variable name (e.g., "ranks") identifies what gets updated
- Same variable name = update in place
- Different variable name = new variable
Loop-Carried Dependencies:
with sG.builder.iter.loop(10):
# ✅ Correct - updates 'values' variable
values = sG.builder.var("values", values * 2.0)
# ❌ Incorrect - creates new variable each iteration (not updated)
with sG.builder.iter.loop(10):
values = values * 2.0 # Doesn't propagate across iterations
loop_range(start, end, step=1)¶
Loop with range (like Python's range()).
with sG.builder.iter.loop_range(0, 100, step=1):
# Iteration variable accessible as special variable
pass
Parameters:
- start: Starting value (int)
- end: Ending value (exclusive, int)
- step: Step size (default: 1)
Example:
@algorithm
def gradual_decay(sG):
values = sG.nodes(100.0)
with sG.builder.iter.loop_range(0, 100):
# Decay by 1% each iteration
values = sG.builder.var("values", values * 0.99)
return values
Note: Iteration variable access is limited in current implementation.
Convergence-Based Loops¶
until_converged(tolerance=1e-6, max_iter=1000, check_every=1)¶
Loop until values converge.
with sG.builder.iter.until_converged(tolerance=1e-6):
values = sG.builder.var("values", updated_values)
Parameters:
- tolerance: Convergence threshold (float, default: 1e-6)
- max_iter: Maximum iterations (int, default: 1000)
- check_every: Check convergence every N iterations (default: 1)
Returns: Context manager
Convergence criterion:
Example - PageRank with convergence:
@algorithm
def pagerank_converged(sG, tol=1e-6, damping=0.85):
ranks = sG.nodes(1.0 / sG.N)
deg = ranks.degrees()
with sG.builder.iter.until_converged(tolerance=tol, max_iter=1000):
contrib = ranks / (deg + 1e-9)
neighbor_sum = sG @ contrib
ranks = sG.builder.var("ranks",
damping * neighbor_sum + (1 - damping) / sG.N
)
return ranks.normalize()
Performance tip:
# Check convergence less frequently for speed
with sG.builder.iter.until_converged(tolerance=1e-6, check_every=10):
# 10x fewer convergence checks
values = sG.builder.var("values", updated)
while_condition(condition_var, max_iter=1000)¶
Loop while condition is true.
Parameters:
- condition_var: VarHandle (scalar, loop continues while > 0)
- max_iter: Maximum iterations (safety limit)
Example - Flood fill:
@algorithm
def flood_fill(sG, source_mask):
filled = source_mask
changed = sG.builder.core.reduce_scalar(source_mask, "sum")
with sG.builder.iter.while_condition(changed, max_iter=1000):
# Propagate to neighbors
neighbor_filled = sG @ filled
newly_filled = neighbor_filled > 0.0
# Update
old_sum = filled.reduce("sum")
filled = sG.builder.var("filled", newly_filled)
new_sum = filled.reduce("sum")
# Check if anything changed
changed = sG.builder.var("changed", new_sum - old_sum)
return filled
Update Strategies¶
set_strategy(strategy)¶
Set update strategy for the loop.
sG.builder.iter.set_strategy("async")
with sG.builder.iter.loop(100):
# Updates happen asynchronously
pass
Strategies:
"sync"- Synchronous updates (default)- All nodes updated simultaneously
- Uses previous iteration's values
-
Deterministic
-
"async"- Asynchronous updates - Nodes updated in order
- Uses latest available values
-
Non-deterministic but often faster convergence
-
"random"- Random order updates - Nodes updated in random order each iteration
- Non-deterministic
Example - Asynchronous Label Propagation:
@algorithm
def lpa_async(sG, max_iter=10):
labels = sG.nodes(unique=True)
# Set async strategy
sG.builder.iter.set_strategy("async")
with sG.builder.iter.loop(max_iter):
labels = sG.builder.graph_ops.neighbor_mode_update(
labels, include_self=True, ordered=True
)
return labels
Comparison:
| Strategy | Deterministic | Convergence Speed | Use Case |
|---|---|---|---|
sync |
✅ Yes | Moderate | PageRank, most algorithms |
async |
❌ No | Fast | Label propagation, Gauss-Seidel |
random |
❌ No | Variable | Stochastic algorithms |
with_schedule(schedule)¶
Use custom update schedule.
# Update every 2 iterations
schedule = sG.builder.iter.every_n_iterations(2)
with sG.builder.iter.loop(100):
with sG.builder.iter.with_schedule(schedule):
values = sG.builder.var("values", expensive_update)
Schedules:
- every_n_iterations(n) - Update every N iterations
- exponential_backoff(base=2) - Update less frequently over time
- custom_schedule(iterations_list) - Update at specific iterations
Example - Progressive refinement:
@algorithm
def progressive_update(sG):
fast_values = sG.nodes(1.0)
slow_values = sG.nodes(1.0)
with sG.builder.iter.loop(100):
# Fast update every iteration
fast_values = sG.builder.var("fast", fast_values * 0.99)
# Slow update every 10 iterations
with sG.builder.iter.with_schedule(
sG.builder.iter.every_n_iterations(10)
):
slow_values = sG.builder.var("slow", expensive_computation())
return fast_values + slow_values
Loop Control¶
break_if(condition)¶
Break loop if condition is true.
with sG.builder.iter.loop(1000):
# ...
converged = max_diff < tolerance
sG.builder.iter.break_if(converged)
Parameters:
- condition: VarHandle (scalar boolean, break if > 0)
Example:
@algorithm
def early_stopping(sG, threshold=1e-6):
values = sG.nodes(1.0)
with sG.builder.iter.loop(1000):
old_values = values
values = sG.builder.var("values", update_values(values))
# Compute max difference
diff = sG.builder.core.abs(values - old_values)
max_diff = diff.reduce("max")
# Break if converged
has_converged = max_diff < threshold
sG.builder.iter.break_if(has_converged)
return values
continue_if(condition)¶
Skip rest of iteration if condition is false.
with sG.builder.iter.loop(100):
should_update = check_condition()
sG.builder.iter.continue_if(should_update)
# This only runs if should_update is true
values = sG.builder.var("values", expensive_update)
Example - Conditional updates:
@algorithm
def conditional_propagation(sG, max_iter=100):
values = sG.nodes(1.0)
with sG.builder.iter.loop(max_iter):
# Check if any values are active
has_active = (values > 0.01).reduce("sum")
# Skip iteration if nothing is active
sG.builder.iter.continue_if(has_active)
# Propagate (only if active nodes exist)
values = sG.builder.var("values", sG @ values * 0.9)
return values
Nested Loops¶
Loops can be nested:
with sG.builder.iter.loop(outer_iters):
# Outer loop body
with sG.builder.iter.loop(inner_iters):
# Inner loop body
values = sG.builder.var("values", updated)
Example - Two-phase algorithm:
@algorithm
def two_phase(sG, outer=10, inner=50):
values = sG.nodes(1.0)
with sG.builder.iter.loop(outer):
# Phase 1: Fast updates
with sG.builder.iter.loop(inner):
values = sG.builder.var("values", values * 0.99)
# Phase 2: Refinement (runs once per outer iteration)
values = sG.builder.var("values", values.normalize())
return values
Note: Deep nesting can impact performance. Keep nesting depth reasonable.
Iteration Metadata¶
iteration_count()¶
Get current iteration number (within loop).
with sG.builder.iter.loop(100):
iter_num = sG.builder.iter.iteration_count()
# Use iter_num in computation
Returns: VarHandle (scalar, current iteration 0-based)
Example - Decay schedule:
@algorithm
def scheduled_decay(sG, max_iter=100):
values = sG.nodes(100.0)
with sG.builder.iter.loop(max_iter):
iter_num = sG.builder.iter.iteration_count()
# Decay rate decreases over time
decay_rate = 0.99 - (iter_num / max_iter) * 0.01
values = sG.builder.var("values", values * decay_rate)
return values
total_iterations()¶
Get total number of iterations (after loop completes).
with sG.builder.iter.until_converged(tolerance=1e-6) as loop_info:
values = sG.builder.var("values", updated)
total = loop_info.total_iterations()
Note: Only available after loop completes, not within loop.
Common Patterns¶
Fixed Iterations¶
Simple fixed-count loop:
Convergence Detection¶
Loop until change is small:
with sG.builder.iter.until_converged(tolerance=1e-6, max_iter=1000):
old = values
values = sG.builder.var("values", update_function(values))
Early Stopping¶
Manual convergence check:
with sG.builder.iter.loop(1000):
old = values
values = sG.builder.var("values", update_function(values))
diff = sG.builder.core.abs(values - old).reduce("max")
converged = diff < tolerance
sG.builder.iter.break_if(converged)
Asynchronous Updates¶
Non-deterministic but faster:
sG.builder.iter.set_strategy("async")
with sG.builder.iter.loop(100):
values = sG.builder.var("values", update_function(values))
Multi-Variable Updates¶
Update multiple variables:
with sG.builder.iter.loop(100):
new_x = update_x(x, y)
new_y = update_y(x, y)
x = sG.builder.var("x", new_x)
y = sG.builder.var("y", new_y)
Phased Updates¶
Different operations at different phases:
with sG.builder.iter.loop(100):
# Always do fast update
values = sG.builder.var("values", fast_update(values))
# Periodically do slow refinement
iter_num = sG.builder.iter.iteration_count()
is_refinement_iter = (iter_num % 10) == 0
with sG.builder.iter.continue_if(is_refinement_iter):
values = sG.builder.var("values", slow_refinement(values))
Performance Considerations¶
Convergence Checking Overhead¶
# ❌ Expensive - checks every iteration
with sG.builder.iter.until_converged(tolerance=1e-6, check_every=1):
values = sG.builder.var("values", update)
# ✅ Better - checks every 10 iterations
with sG.builder.iter.until_converged(tolerance=1e-6, check_every=10):
values = sG.builder.var("values", update)
Synchronous vs Asynchronous¶
# Sync: Deterministic, moderate speed
sG.builder.iter.set_strategy("sync")
# Async: Non-deterministic, often 2-5x faster convergence
sG.builder.iter.set_strategy("async")
Loop Body Complexity¶
Keep loop bodies focused:
# ✅ Good - focused loop body
with sG.builder.iter.loop(100):
values = sG.builder.var("values", core_update(values))
# ❌ Avoid - complex loop body
with sG.builder.iter.loop(100):
# Many operations make loop harder to optimize
a = complex_op_1()
b = complex_op_2()
c = complex_op_3()
values = sG.builder.var("values", combine(a, b, c))
Limitations¶
No Dynamic Loop Counts¶
Loop count must be known at algorithm definition time:
# ❌ Doesn't work - can't be dynamic
param = load_from_somewhere()
with sG.builder.iter.loop(param):
pass
# ✅ Works - pass as parameter
@algorithm
def my_algo(sG, max_iter):
with sG.builder.iter.loop(max_iter):
pass
No Arbitrary Break Conditions¶
Break conditions must be computable within the IR:
# ❌ Can't use Python conditionals
if some_python_condition:
break
# ✅ Use IR-based conditions
sG.builder.iter.break_if(ir_condition_var)
Variable Scope¶
Variables updated in loops must use sG.builder.var():
# ❌ Doesn't create loop-carried dependency
with sG.builder.iter.loop(10):
values = values * 2.0 # Creates new variable each time
# ✅ Correct
with sG.builder.iter.loop(10):
values = sG.builder.var("values", values * 2.0)
Examples¶
PageRank¶
@algorithm
def pagerank(sG, damping=0.85, max_iter=100, tol=1e-6):
ranks = sG.nodes(1.0 / sG.N)
deg = ranks.degrees()
with sG.builder.iter.until_converged(tolerance=tol, max_iter=max_iter):
contrib = ranks / (deg + 1e-9)
neighbor_sum = sG @ contrib
# Handle sinks
is_sink = (deg == 0.0)
sink_mass = is_sink.where(ranks, 0.0).reduce("sum")
ranks = sG.builder.var("ranks",
damping * neighbor_sum +
(1 - damping) / sG.N +
damping * sink_mass / sG.N
)
return ranks.normalize()
Label Propagation (Async)¶
@algorithm
def lpa_async(sG, max_iter=10):
labels = sG.nodes(unique=True)
sG.builder.iter.set_strategy("async")
with sG.builder.iter.loop(max_iter):
labels = sG.builder.graph_ops.neighbor_mode_update(
labels, include_self=True, ordered=True
)
return labels
Belief Propagation¶
@algorithm
def belief_propagation(sG, max_iter=50, damping=0.5):
beliefs = sG.nodes(1.0)
messages = sG.nodes(0.0)
with sG.builder.iter.loop(max_iter):
# Update messages
new_messages = sG @ beliefs
messages = sG.builder.var("messages",
damping * new_messages + (1 - damping) * messages
)
# Update beliefs
beliefs = sG.builder.var("beliefs",
beliefs * messages.normalize()
)
return beliefs.normalize()
Iterative Refinement¶
@algorithm
def iterative_refinement(sG, max_iter=100):
coarse = sG.nodes(1.0)
fine = sG.nodes(0.0)
with sG.builder.iter.loop(max_iter):
# Coarse update (every iteration)
coarse = sG.builder.var("coarse", sG @ coarse * 0.9)
# Fine update (every 10 iterations)
iter_num = sG.builder.iter.iteration_count()
is_fine_iter = (iter_num % 10) == 0
with sG.builder.iter.continue_if(is_fine_iter):
fine = sG.builder.var("fine", expensive_refinement(coarse))
return coarse + fine
See Also¶
- CoreOps API - Operations within loop bodies
- GraphOps API - Neighbor operations in iterations
- PageRank Tutorial - Iterative algorithm example
- LPA Tutorial - Asynchronous iteration example