Local Search Solver
The Local Search solver uses iterative improvement to find good solutions quickly. It's the recommended solver for large problems (>10,000 objects).
How It Works
Local Search starts with the current assignment and repeatedly makes "moves" that improve the objective:
- Start with initial assignment
- Generate candidate moves (e.g., move object X to container Y)
- Evaluate each move's impact on objective
- Apply the best improving move
- Repeat until no improving moves found (or hit limits)
This finds a local optimum - a solution where no single move improves things further.
Quick Example
- Python
- C++
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
timeLimitMs=30000, # Stop after 30 seconds
movesLimit=100000, # Or after 100K iterations
)
)
)
solution = solver.solve()
# Check termination reason
print(f"Stopped because: {solution.profile.terminationReason}")
print(f"Iterations: {solution.profile.iterations}")
print(f"Time: {solution.profile.solveTime}ms")
LocalSearchSolverSpec spec;
spec.moveTypeList()->push_back(
ProblemSolver::makeMoveTypeSpec(SingleMoveTypeSpec()));
spec.solveTime() = 30; // Stop after 30 seconds
spec.stopAfterMoves() = 100000; // Or after 100K iterations
solver.addSolver(spec);
auto solution = solver.solve();
// Check solve time
std::cout << "Time: " << *solution.problemProfile()->solveSec() << "s\n";
Configuration Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
solveTime | int | No limit | Maximum solve time in milliseconds |
stopAfterMoves | int | No limit | Maximum number of moves to apply |
moveTypeList | list<MoveTypeSpec> | Auto | Which move types to use (see below) |
randomSeed | int | Random | Random seed for reproducibility |
enableObjectPotentialSorting | bool | false | Enable potential-based object sorting |
minHotObjects | int | 1 | Minimum hot objects to consider |
Move Types
Local Search explores different types of moves. Each move type searches a different "neighborhood" of solutions. See the Move Types Reference for complete documentation of all 27 move types.
Common Move Types
Single: Move one object to a different container
- When: Always useful - most fundamental move type
- Cost: O(objects × containers) per iteration
- Example: Move task5 from host1 to host3
Swap: Swap two objects between containers
- When: Capacity constrained (can't just add objects)
- Cost: O(objects²) per iteration
- Example: Swap task5 on host1 with task8 on host3
TripleLoop: Try complex multi-object rearrangements
- When: Need to escape local optima
- Cost: Expensive, O(objects³)
- Example: Move 3+ objects in a cycle
Specialized Move Types
KLSearch: Kernighan-Lin style search with sequences of moves
- When: Graph partitioning-style problems
- Cost: Expensive but powerful
Chain Moves: Move sequences of objects in a chain
- When: Moving objects creates opportunities for other moves
- Cost: Medium
- Best: SingleEndChain (recommended over SingleChain)
Group Moves: Move entire groups together
- When: Using MoveGroupSpec or colocation goals
- Cost: Depends on group sizes
- Example: ColocateGroups
Fixed Source/Dest: Only consider moves from/to specific containers
- When: Draining specific containers (
ToFree) or filling specific containers - Cost: Reduced search space, faster
- Example: FixedSource for draining
Configuring Move Types
- Python
- C++
from rebalancer.specs import (
LocalSearchSolverSpec,
MoveTypeSpec,
SingleMoveTypeSpec,
SolverSpec,
SwapMoveTypeSpec,
)
# Default: auto-selected move types
solver.add_solver(SolverSpec(localSearchSolverSpec=LocalSearchSolverSpec()))
# Custom: specify exact move types to use
solver.add_solver(
SolverSpec(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
MoveTypeSpec(singleMoveTypeSpec=SingleMoveTypeSpec()),
MoveTypeSpec(swapMoveTypeSpec=SwapMoveTypeSpec()),
]
)
)
)
#include <rebalancer/interface/thrift/gen-cpp2/SolverSpecs_types.h>
using namespace facebook::rebalancer::interface;
// Default: auto-selected move types
LocalSearchSolverSpec spec;
solver.addSolver(spec);
// Custom: specify exact move types
LocalSearchSolverSpec customSpec;
MoveTypeSpec singleSpec;
singleSpec.singleMoveTypeSpec_ref() = SingleMoveTypeSpec();
customSpec.moveTypeList_ref()->push_back(singleSpec);
MoveTypeSpec swapSpec;
swapSpec.swapMoveTypeSpec_ref() = SwapMoveTypeSpec();
customSpec.moveTypeList_ref()->push_back(swapSpec);
solver.addSolver(customSpec);
Termination Conditions
Local Search stops when:
- No improving move found - Reached local optimum (
UNABLE_TO_FIND_MORE_MOVES) - Move limit reached - Hit
stopAfterMoves(HIT_MOVE_LIMIT) - Time limit reached - Hit
solveTime(HIT_TIME_LIMIT) - Plateau timeout - Stuck in plateau too long (
HIT_PLATEAU_TIME)
Check termination reason from the solver report:
- Python
- C++
# Access the solver report (first solver if multiple)
solver_report = solution["solverSummaries"][0]
end_reason = solver_report["endReason"]
if end_reason == "UNABLE_TO_FIND_MORE_MOVES":
print("Found local optimum")
elif end_reason == "HIT_MOVE_LIMIT":
print("Hit iteration limit - may improve with more moves")
elif end_reason == "HIT_TIME_LIMIT":
print("Hit time limit - may improve with more time")
// Access the solver report (first solver if multiple)
auto& solverReport = solution.solverSummaries[0];
if (solverReport.endReason == EndReason::UNABLE_TO_FIND_MORE_MOVES) {
std::cout << "Found local optimum\n";
} else if (solverReport.endReason == EndReason::HIT_MOVE_LIMIT) {
std::cout << "Hit iteration limit - may improve with more moves\n";
} else if (solverReport.endReason == EndReason::HIT_TIME_LIMIT) {
std::cout << "Hit time limit - may improve with more time\n";
}
Performance Characteristics
Scalability
| Problem Size | Typical Time | Iterations |
|---|---|---|
| 100 objects, 10 containers | <1 second | 100-1,000 |
| 1,000 objects, 100 containers | 1-5 seconds | 1,000-10,000 |
| 10,000 objects, 1,000 containers | 10-60 seconds | 10,000-100,000 |
| 100,000 objects, 10,000 containers | 1-10 minutes | 100,000-1,000,000 |
Memory Usage
- Per object: ~100-200 bytes
- Per container: ~50-100 bytes
- Example: 100K objects + 10K containers ≈ 20MB
Parallelization
Local Search uses multi-threading for move evaluation:
- Move generation: Can be parallelized across move types
- Move evaluation: Parallelized across candidate moves
- Cores used: Effectively utilizes multiple cores (typically 2-8+)
- Note: Some move types like SingleGreedy are single-threaded
Solution Quality
Local Search provides no optimality guarantee, but empirically:
- Typically 95-99% of optimal for well-structured problems
- Quality improves with more time/iterations
- Quality depends on initial assignment
Quality Factors
Good quality when:
- Good initial assignment
- Well-balanced problem
- Sufficient time/moves
- Appropriate move types
Poor quality when:
- Terrible initial assignment (many broken constraints)
- Highly constrained problem (few feasible solutions)
- Insufficient time/moves
- Wrong move types for problem structure
Improving Solution Quality
Increase Limits
- Python
- C++
# Give more time
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
timeLimitMs=300000, # 5 minutes instead of 30 seconds
movesLimit=1000000, # 1M moves instead of 100K
)
)
)
// Give more time
LocalSearchSolverSpec spec;
spec.moveTypeList()->push_back(
ProblemSolver::makeMoveTypeSpec(SingleMoveTypeSpec()));
spec.solveTime() = 300; // 5 minutes instead of 30 seconds
spec.stopAfterMoves() = 1000000; // 1M moves instead of 100K
solver.addSolver(spec);
Better Initial Assignment
# Start with a better assignment
# E.g., pre-balance object counts before running solver
initial_assignment = distribute_evenly(objects, containers)
solver.set_assignment(initial_assignment)
Enable More Move Types
By default, Local Search auto-selects appropriate move types. To use all available move types or a custom set, specify them explicitly using moveTypeList:
- Python
- C++
from rebalancer.specs import (
LocalSearchSolverSpec,
MoveTypeSpec,
SingleChainMoveTypeSpec,
SingleMoveTypeSpec,
SolverSpec,
SwapMoveTypeSpec,
TripleLoopMoveTypeSpec,
)
# Use a comprehensive set of move types
solver.add_solver(
SolverSpec(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
MoveTypeSpec(singleMoveTypeSpec=SingleMoveTypeSpec()),
MoveTypeSpec(swapMoveTypeSpec=SwapMoveTypeSpec()),
MoveTypeSpec(tripleLoopMoveTypeSpec=TripleLoopMoveTypeSpec()),
MoveTypeSpec(singleChainMoveTypeSpec=SingleChainMoveTypeSpec()),
]
)
)
)
// Use a comprehensive set of move types
LocalSearchSolverSpec spec;
std::vector<MoveTypeSpec> moveTypes;
MoveTypeSpec single;
single.singleMoveTypeSpec_ref() = SingleMoveTypeSpec();
moveTypes.push_back(single);
MoveTypeSpec swap;
swap.swapMoveTypeSpec_ref() = SwapMoveTypeSpec();
moveTypes.push_back(swap);
spec.moveTypeList_ref() = moveTypes;
solver.addSolver(spec);
Run Multiple Times
from rebalancer import ProblemSolver
from rebalancer.specs import LocalSearchSolverSpec, SolverSpec
# Try multiple random seeds, pick best
best_solution = None
best_objective = float("inf")
for seed in range(10):
solver = ProblemSolver(service_name="rebalancer", service_scope="seed-search")
# ... set up problem ...
solver.add_solver(
SolverSpec(
localSearchSolverSpec=LocalSearchSolverSpec(randomSeed=seed)
)
)
solution = solver.solve()
objective = solution["finalObjective"]["value"]
if objective < best_objective:
best_objective = objective
best_solution = solution
# best_solution is the best of 10 runs
Common Patterns
Fast Interactive Rebalancing
Quick responses for interactive tools:
- Python
- C++
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
timeLimitMs=5000, # 5 second limit
)
)
)
LocalSearchSolverSpec spec;
spec.moveTypeList()->push_back(
ProblemSolver::makeMoveTypeSpec(SingleMoveTypeSpec()));
spec.solveTime() = 5; // 5 second limit
solver.addSolver(spec);
Production Rebalancing
Balance speed and quality:
- Python
- C++
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
timeLimitMs=60000, # 1 minute
movesLimit=500000, # 500K moves
)
)
)
LocalSearchSolverSpec spec;
spec.moveTypeList()->push_back(
ProblemSolver::makeMoveTypeSpec(SingleMoveTypeSpec()));
spec.solveTime() = 60; // 1 minute
spec.stopAfterMoves() = 500000; // 500K moves
solver.addSolver(spec);
Offline Optimization
Take time to find best solution:
- Python
- C++
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
timeLimitMs=3600000, # 1 hour
# No move limit - run until no improvements
)
)
)
LocalSearchSolverSpec spec;
spec.moveTypeList()->push_back(
ProblemSolver::makeMoveTypeSpec(SingleMoveTypeSpec()));
spec.solveTime() = 3600; // 1 hour
// No move limit - run until no improvements
solver.addSolver(spec);
Draining Containers
Use FixedSource move type for draining specific containers (e.g., with ToFreeSpec):
- Python
- C++
from rebalancer.specs import (
LocalSearchSolverSpec,
MoveTypeSpec,
SingleFixedSourceMoveTypeSpec,
SolverSpec,
)
# When draining containers, use FixedSource to only consider
# moves from the containers being drained.
solver.add_solver(
SolverSpec(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
MoveTypeSpec(
singleFixedSourceMoveTypeSpec=SingleFixedSourceMoveTypeSpec()
)
]
)
)
)
// When draining containers, use FixedSource
LocalSearchSolverSpec spec;
MoveTypeSpec fixedSourceSpec;
fixedSourceSpec.singleFixedSourceMoveTypeSpec_ref() = SingleFixedSourceMoveTypeSpec();
spec.moveTypeList_ref() = {fixedSourceSpec};
solver.addSolver(spec);
Debugging Poor Results
Problem: Solution not improving
Diagnosis:
print(f"Iterations: {solution["profile"].iterations}")
print(f"Time: {solution["profile"].solveTime}ms")
print(f"Reason: {solution["profile"].terminationReason}")
Solutions:
- Increase time/move limits
- Check if initial assignment is extremely poor
- Verify goals/constraints are achievable
Problem: Slow convergence
Diagnosis:
- Many iterations but small improvements
- Terminating due to limits, not local optimum
Solutions:
- Try different move types
- Improve initial assignment
- Simplify problem (fewer goals/constraints)
Problem: Stuck in local optimum
Diagnosis:
- Terminates quickly with NO_IMPROVING_MOVE
- Objective value seems poor compared to expectations
Solutions:
- Use more powerful move types (TripleLoop, KLSearch)
- Try multiple runs with different seeds
- Use better initial assignment
- Consider using Optimal solver for comparison
Problem: Too slow
Diagnosis:
- Taking too long for problem size
- Not enough iterations in time limit
Solutions:
- Use simpler move types (SingleMove only)
- Reduce time limit (accept worse solution)
- Simplify problem
- Check for performance bottlenecks in goals/constraints
Comparison with Optimal Solver
| Aspect | Local Search | Optimal |
|---|---|---|
| Solution quality | 95-99% of optimal | 100% optimal* |
| Speed | Fast (seconds) | Slow (minutes to hours) |
| Scalability | 1M+ objects | <10K objects |
| Guarantees | None | Optimality gap |
| Determinism | Random (without seed) | Deterministic |
| Memory | Low | High (can OOM) |
* Given enough time
Recommendation: Use Local Search for production, Optimal for validation/small problems.
Advanced: Multi-Stage Solving
Local Search supports multi-stage solving using LocalSearchStageSolverSpec, where each stage can use different move types and focus on different objectives:
- Python
- C++
from rebalancer.specs import (
LocalSearchSolverSpec,
LocalSearchStageSolverSpec,
LocalSearchStageSpec,
MoveTypeSpec,
SingleFastMoveTypeSpec,
SingleMoveTypeSpec,
SolverSpec,
)
# Stage 1: Fast coarse moves
stage1 = LocalSearchStageSpec(
name="coarse",
begin=0,
end=2, # Focus on objectives 0-1
solverSpec=LocalSearchSolverSpec(
moveTypeList=[
MoveTypeSpec(singleFastMoveTypeSpec=SingleFastMoveTypeSpec())
],
stopAfterMoves=10000,
),
)
# Stage 2: Fine-tuning with more expensive moves
stage2 = LocalSearchStageSpec(
name="fine-tuning",
begin=0,
end=3, # Focus on all objectives 0-2
solverSpec=LocalSearchSolverSpec(
moveTypeList=[MoveTypeSpec(singleMoveTypeSpec=SingleMoveTypeSpec())],
stopAfterMoves=5000,
),
)
solver.add_solver(
SolverSpec(
localSearchStageSolverSpec=LocalSearchStageSolverSpec(
stageSpecs=[stage1, stage2],
solveTime=60000, # Overall time limit
)
)
)
#include <rebalancer/interface/thrift/gen-cpp2/SolverSpecs_types.h>
using namespace facebook::rebalancer::interface;
// Stage 1: Fast coarse moves
LocalSearchStageSpec stage1;
stage1.name_ref() = "coarse";
stage1.begin_ref() = 0;
stage1.end_ref() = 2; // Focus on objectives 0-1
LocalSearchSolverSpec stage1Solver;
MoveTypeSpec fastMove;
fastMove.singleFastMoveTypeSpec_ref() = SingleFastMoveTypeSpec();
stage1Solver.moveTypeList_ref() = {fastMove};
stage1Solver.stopAfterMoves_ref() = 10000;
stage1.solverSpec_ref() = stage1Solver;
// Stage 2: Fine-tuning
LocalSearchStageSpec stage2;
stage2.name_ref() = "fine-tuning";
stage2.begin_ref() = 0;
stage2.end_ref() = 3; // Focus on all objectives
LocalSearchSolverSpec stage2Solver;
MoveTypeSpec singleMove;
singleMove.singleMoveTypeSpec_ref() = SingleMoveTypeSpec();
stage2Solver.moveTypeList_ref() = {singleMove};
stage2Solver.stopAfterMoves_ref() = 5000;
stage2.solverSpec_ref() = stage2Solver;
// Multi-stage solver
LocalSearchStageSolverSpec multiStage;
multiStage.stageSpecs_ref() = {stage1, stage2};
multiStage.solveTime_ref() = 60000; // Overall time limit
solver.addSolver(multiStage);
See Solver Strategies for multi-stage solving.
Troubleshooting
Problem: Non-deterministic results
Cause: Random seed not set
Solution:
- Python
- C++
solver.addSolver(SolverSpecs(localSearchSolverSpec=LocalSearchSolverSpec(seed=42)))
LocalSearchSolverSpec spec;
spec.moveTypeList()->push_back(
ProblemSolver::makeMoveTypeSpec(SingleMoveTypeSpec()));
spec.randomSeed() = 42;
solver.addSolver(spec);
Problem: Violating constraints
Cause: Constraints initially broken, being treated as goals
Solution: Check initial assignment, or increase constraint penalty:
solver.add_constraint(spec, invalid_cost=1000.0)
Problem: Different results each run
Cause: Default random seed varies
Solution: Set explicit seed for reproducibility
Next Steps
- Learn Optimal Solver: Optimal Solver Guide
- Performance Tuning: Performance Guide
- Multi-Stage Solving: Solver Strategies
Related Documentation
- Solver Overview - Choosing between solvers
- Performance Guide - Tuning for speed