SingleFast
Move Type: Basic Complexity: O(objects × containers) with early termination
Move one object at a time but stop early after finding an improvement. Faster than Single but may miss better moves.
Overview
SingleFast evaluates moving each object from the hot container to every possible destination container, but uses early termination to improve performance. After fully exploring minHotObjects (default: 1), it returns the best improving move found so far.
Use when:
- Speed is more important than finding the absolute best move
- Want faster convergence than Single
- Willing to accept local optima for performance
Avoid when:
- Need thorough exploration of all moves
- Problem is small enough for complete Single search
- Quality is more important than speed
Quick Example
- Python
- C++
# Use SingleFast for faster convergence (stops after finding improvement)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleFastMoveTypeSpec(), # Fast early-exit single moves
]
)
)
)
void quickExample() {
// Use SingleFast for faster convergence (stops after finding improvement)
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("task");
solver.setContainerName("host");
LocalSearchSolverSpec solverSpec;
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleFastMoveTypeSpec() =
SingleFastMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2", "task3"}},
{"host1", {}},
{"host2", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 2.0},
{"task1", 1.5},
{"task2", 2.5},
{"task3", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(balanceSpec, 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
destinationsToExplore | DestinationsToExploreOptions | No | null | Restrict which containers to explore |
minHotObjects | int32 | No | 1 | Minimum objects to fully explore before returning |
How It Works
Given a hot container (the container contributing most to the objective):
- Select object: Pick first object from the hot container
- Evaluate all destinations: Test moving this object to every possible destination container (in parallel)
- Check improvement: If any move improves the objective, remember the best one
- Early exit check: If we've explored
minHotObjectsand found an improving move, stop and return it - Continue if needed: If no improvement found, move to next object
- Repeat: Until improvement found or all objects exhausted
Visual Example
Iteration 1 - Explore obj1:
┌─────────────┐ Test all destinations (parallel)
│ Hot │ ──> dest1: +5 improvement ✓
│ Container │ ──> dest2: -2 worse
│ • obj1 ← │ ──> dest3: +3 improvement
│ • obj2 │
│ • obj3 │ Best: obj1→dest1 (+5)
└─────────────┘
minHotObjects=1 reached → STOP, return obj1→dest1
Without early exit (Single), would also explore:
obj2 → all dests
obj3 → all dests
Difference from Single
| Aspect | Single | SingleFast |
|---|---|---|
| Exploration | All objects, all destinations | Stops after minHotObjects if improvement found |
| Parallelization | All moves in parallel | Per-object destinations in parallel |
| Guarantee | Finds best single move | Finds early improving move |
| Speed | Slower | Faster |
Complexity
Minimum moves evaluated: O(minHotObjects × C) Maximum moves evaluated: O(N × C) (same as Single if no improvements found)
Where:
- N = number of objects in hot container
- C = number of destination containers
- minHotObjects = parameter (default: 1)
Example - Best case (improvement found early):
- Hot container has 1000 objects
- System has 100 containers
- minHotObjects = 1 (default)
- Moves evaluated = 1 × 100 = 100 moves (vs 100,000 for Single)
Example - Worst case (no improvements):
- Moves evaluated = 1000 × 100 = 100,000 moves (same as Single)
Usage Patterns
Basic Fast Configuration
Default usage for faster convergence:
- Python
- C++
# SingleFast is good default for faster convergence
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleFastMoveTypeSpec(), # Use fast variant by default
]
)
)
)
void basicConfiguration() {
// SingleFast is good default for faster convergence
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
LocalSearchSolverSpec solverSpec;
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleFastMoveTypeSpec() =
SingleFastMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1"}},
{"host1", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 1.0},
{"task1", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(balanceSpec, 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Combined with Single
Try SingleFast first, fall back to Single:
- Python
- C++
# Try fast moves first, then thorough search if needed
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleFastMoveTypeSpec(), # Try fast moves first
SingleMoveTypeSpec(), # Fall back to thorough search
]
)
)
)
void combinedWithSingle() {
// Try fast moves first, then thorough search if needed
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
LocalSearchSolverSpec solverSpec;
// Add SingleFast
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleFastMoveTypeSpec() =
SingleFastMoveTypeSpec{};
// Add Single as fallback
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleMoveTypeSpec() = SingleMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2"}},
{"host1", {}},
{"host2", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 2.0},
{"task1", 1.5},
{"task2", 1.5},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Explore More Objects Before Stopping
Increase minHotObjects for better quality:
- Python
- C++
# Explore 5 objects before stopping (better quality)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleFastMoveTypeSpec(
minHotObjects=5, # Explore 5 objects before early exit
),
]
)
)
)
void moreObjects() {
// Explore 5 objects before stopping (better quality)
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
LocalSearchSolverSpec solverSpec;
SingleFastMoveTypeSpec fastSpec;
fastSpec.minHotObjects() = 5; // Explore 5 objects before early exit
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleFastMoveTypeSpec() = fastSpec;
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2", "task3", "task4"}},
{"host1", {}},
{"host2", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 1.0},
{"task1", 2.0},
{"task2", 1.5},
{"task3", 2.5},
{"task4", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Restrict Destinations
Limit search space for even faster convergence:
- Python
- C++
# Only explore moves within the same rack
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleFastMoveTypeSpec(
destinationsToExplore=MoveToCurrentScopeItemSpec(
scopeNameForExploringMovesToCurrentScopeItem="rack",
),
),
]
)
)
)
void restrictDestinations() {
// Only explore moves within the same rack
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
// Define rack scope
solver.addScope(
"rack",
std::map<std::string, std::string>{
{"host0", "rack0"},
{"host1", "rack0"},
{"host2", "rack1"},
{"host3", "rack1"},
});
LocalSearchSolverSpec solverSpec;
SingleFastMoveTypeSpec fastSpec;
MoveToCurrentScopeItemSpec destSpec;
destSpec.scopeNameForExploringMovesToCurrentScopeItem() = "rack";
DestinationsToExploreOptions destOptions;
destOptions.moveToCurrentScopeItem() = destSpec;
fastSpec.destinationsToExplore() = std::move(destOptions);
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleFastMoveTypeSpec() = fastSpec;
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2"}},
{"host1", {}},
{"host2", {"task3"}},
{"host3", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 1.0},
{"task1", 1.0},
{"task2", 1.0},
{"task3", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Interactive Applications
Fast responses for UI/dashboards:
- Python
- C++
# Fast solver for UI/dashboard (1 second limit)
solver.addGoal(
GoalSpecs(
balanceSpec=BalanceSpec(name="balance-cpu", scope="host", dimension="cpu")
),
weight=1.0,
)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
solveTime=1, # 1 second limit for interactive response
moveTypeList=[
SingleFastMoveTypeSpec(), # Fast moves only
],
)
)
)
void interactive() {
// Fast solver for UI/dashboard (1 second limit)
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("task");
solver.setContainerName("host");
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2", "task3", "task4"}},
{"host1", {}},
{"host2", {}},
{"host3", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 2.0},
{"task1", 1.5},
{"task2", 2.5},
{"task3", 1.0},
{"task4", 2.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
LocalSearchSolverSpec solverSpec;
solverSpec.solveTime() = 1; // 1 second limit for interactive response
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleFastMoveTypeSpec() =
SingleFastMoveTypeSpec{};
solver.addSolver(solverSpec);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Performance Characteristics
Scalability
| Problem Size | Objects/Container | Containers | SingleFast (typical) | Single (always) |
|---|---|---|---|---|
| Small | 10 | 10 | <1ms | <1ms |
| Medium | 100 | 100 | 1-5ms | 10-50ms |
| Large | 1,000 | 1,000 | 10-100ms | 0.5-2s |
| Very Large | 10,000 | 10,000 | 100ms-2s | 10-60s |
Note: Times vary based on when improvements are found. Best case is 10-100x faster than Single.
Speedup Factor
Actual speedup depends on problem characteristics:
- Best case (improvements found early): 10-100x faster
- Average case (improvements found midway): 2-10x faster
- Worst case (no improvements): Same as Single
Multi-threading
- Per-object destinations evaluated in parallel
- Scales well up to 8-16 cores
- Less parallel work than Single (explores fewer objects)
Comparison with Variants
| Move Type | Speed | Thoroughness | Quality | Use Case |
|---|---|---|---|---|
| SingleFast | Fast | Early exit | Good | Default fast choice |
| Single | Medium | Complete | Best | Need best single move |
| SingleGreedy | Fastest | Greedy | Fair | Speed critical, single-threaded OK |
| SingleRandomBatches | Fast | Batched | Good | Parallel batching preferred |
Recommendation:
- Use SingleFast as default for faster convergence
- Use Single when quality is critical and time is available
- Use SingleGreedy for single-threaded or extremely time-sensitive cases
Troubleshooting
Problem: SingleFast not much faster than Single
Diagnosis: Few or no improvements being found early
Solutions:
- This is expected for highly constrained or broken initial assignments
- Try improving initial assignment quality
- Consider using SingleGreedy if speed is critical
- Check if problem has many feasible moves
Problem: Solution quality worse than Single
Diagnosis: Early termination missing better moves
Solutions:
- Increase
minHotObjectsto explore more before stopping - Use SingleFast for initial passes, switch to Single for final refinement
- Combine both:
[SingleFastMoveTypeSpec(), SingleMoveTypeSpec()] - Accept the trade-off (speed vs quality)
Problem: Still too slow
Diagnosis: Worst-case behavior (no early improvements)
Solutions:
- Reduce destinations with
destinationsToExplore - Use SingleGreedy for single-threaded speed
- Use SingleRandomBatches for parallel batching
- Check if initial assignment allows any improvements
Problem: Getting stuck in local optima
Diagnosis: Early termination prevents finding escape moves
Solutions:
- Add more powerful move types after SingleFast (Swap, Chain)
- Increase
minHotObjectsto be less greedy - Use Single periodically for thorough search
- Try multiple solver runs with different seeds
Related Move Types
Variants:
- Single - Thorough version without early termination
- SingleGreedy - Even faster with greedy destination selection
- SingleRandomBatches - Parallel batched approach
Complementary:
- Swap - For capacity-constrained problems
- SingleEndChain - For 2-move sequences
When to use which:
- Small problem (<1K objects): Use Single (thoroughness worth the time)
- Medium problem (1-10K objects): Use SingleFast (good speed/quality balance)
- Large problem (>10K objects): Use SingleFast or SingleGreedy
- Interactive/UI: Use SingleFast with low time limits
Source Code
- Thrift definition:
interface/thrift/SolverSpecs.thrift:513 - Implementation:
solver/moves/SingleFastMoveType.h - Tests:
solver/moves/tests/
Next Steps
- Learn about Single for thorough exploration
- Try SingleGreedy for even faster single-threaded speed
- Review Move Types Overview for choosing move types
- Check Performance Guide for tuning