Single
Move Type: Basic Complexity: O(objects × containers)
Move one object at a time to any destination container. This is the most fundamental move type and should be included in almost every Local Search configuration.
Overview
Single evaluates moving each object from the hot container to every possible destination container. It explores all single-object moves in parallel using multi-threading.
Use when:
- Always - this is the foundation of Local Search
- Problems with soft constraints or unconstrained
- Initial solver configuration
Avoid when:
- Need faster convergence (use
SingleFastinstead) - Single-threaded environment required (use
SingleGreedy)
Quick Example
- Python
- C++
# Use Single move type (most basic - move one object at a time)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleMoveTypeSpec(), # Evaluate all single-object moves
]
)
)
)
void quickExample() {
// Use Single move type (most basic - move one object at a time)
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().singleMoveTypeSpec() = SingleMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup problem: imbalanced tasks across hosts
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", 2.0},
{"task1", 1.5},
{"task2", 3.0},
{"task3", 1.0},
{"task4", 2.5},
});
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 |
|---|---|---|---|---|
| (none) | - | - | - | Single move type has no configuration parameters |
How It Works
Given a hot container (the container contributing most to the objective):
- Select object: Pick one object from the hot container (the "hot object")
- Select destination: Pick a different container (the "destination container")
- Evaluate: Test moving the hot object to the destination container
- Repeat: Try all combinations of (hot object, destination container)
- Apply best: Apply the move that improves the objective most
All moves are evaluated in parallel to benefit from multi-threading.
Visual Example
Before: After (if move applied):
┌─────────────┐ ┌─────────────┐
│ Hot │ │ Hot │
│ Container │ │ Container │
│ • obj1 │ ──move obj1──> │ • obj2 │
│ • obj2 │ │ • obj3 │
│ • obj3 │ └─────────────┘
└─────────────┘ ┌─────────────┐
┌─────────────┐ │ Dest │
│ Dest │ │ Container │
│ Container │ │ • obj4 │
│ • obj4 │ │ • obj1 ← │
└─────────────┘ └─────────────┘
Complexity
Moves evaluated per iteration: O(N × C)
Where:
- N = number of objects in hot container
- C = number of destination containers
Example:
- Hot container has 100 objects
- System has 50 containers
- Moves evaluated = 100 × 50 = 5,000 moves
Parallelization: All moves evaluated in parallel using available CPU cores
Usage Patterns
Basic Configuration
Most common usage - include Single in move type list:
- Python
- C++
# Single move type is the foundation - include it in almost all configurations
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleMoveTypeSpec(), # Always start with Single
]
)
)
)
void basicConfiguration() {
// Single move type is the foundation - include it in almost all
// configurations
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
LocalSearchSolverSpec solverSpec;
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleMoveTypeSpec() = SingleMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup simple 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", 1.0},
{"task1", 1.0},
{"task2", 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";
}
Combined with Other Move Types
Typical pattern - Single as foundation, other types for special cases:
- Python
- C++
# Typical pattern: Single as foundation, other types for special cases
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleMoveTypeSpec(), # Try simple moves first
SwapMoveTypeSpec(), # Then try swaps for capacity-constrained
]
)
)
)
void combinedWithOthers() {
// Typical pattern: Single as foundation, other types for special cases
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
LocalSearchSolverSpec solverSpec;
// Add Single move type
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleMoveTypeSpec() = SingleMoveTypeSpec{};
// Add Swap move type
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().swapMoveTypeSpec() = SwapMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup capacity-constrained problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1"}},
{"host1", {"task2"}},
{"host2", {}},
});
solver.addObjectDimension(
"memory",
std::map<std::string, double>{
{"task0", 3.0},
{"task1", 1.0},
{"task2", 4.0},
});
CapacitySpec capacitySpec;
capacitySpec.name() = "memory-capacity";
capacitySpec.scope() = "host";
capacitySpec.dimension() = "memory";
Limit limit;
limit.globalLimit() = 4.0;
capacitySpec.limit() = std::move(limit);
solver.addConstraint(std::move(capacitySpec));
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-memory";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "memory";
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";
}
Load Balancing
Use Single for straightforward load balancing:
- Python
- C++
# Balance CPU across hosts using Single moves
solver.addConstraint(
ConstraintSpecs(
capacitySpec=CapacitySpec(
name="cpu-capacity",
scope="host",
dimension="cpu",
limit=Limit(globalLimit=16.0), # Max 16 cores per host
)
)
)
solver.addGoal(
GoalSpecs(
balanceSpec=BalanceSpec(name="balance-cpu", scope="host", dimension="cpu")
),
weight=1.0,
)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleMoveTypeSpec(), # Single moves are sufficient for balancing
]
)
)
)
void loadBalancing() {
// Balance CPU across hosts using Single moves
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("task");
solver.setContainerName("host");
// Setup imbalanced problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2", "task3", "task4", "task5"}},
{"host1", {}},
{"host2", {}},
{"host3", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 2.0},
{"task1", 3.0},
{"task2", 1.5},
{"task3", 2.5},
{"task4", 1.0},
{"task5", 2.0},
});
// Add capacity constraint
CapacitySpec capacitySpec;
capacitySpec.name() = "cpu-capacity";
capacitySpec.scope() = "host";
capacitySpec.dimension() = "cpu";
Limit limit;
limit.globalLimit() = 16.0; // Max 16 cores per host
capacitySpec.limit() = std::move(limit);
solver.addConstraint(std::move(capacitySpec));
// Add balance goal
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
// Add solver with Single move type
LocalSearchSolverSpec solverSpec;
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleMoveTypeSpec() = SingleMoveTypeSpec{};
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 | Typical Time per Iteration |
|---|---|---|---|
| Small | 10 | 10 | <1ms |
| Medium | 100 | 100 | 10-50ms |
| Large | 1,000 | 1,000 | 0.5-2s |
| Very Large | 10,000 | 10,000 | 10-60s |
Multi-threading
Single move type benefits significantly from multi-threading:
- All object-destination pairs evaluated in parallel
- Scales well up to 8-16 cores
- CPU-bound (evaluation is fast, parallelization helps)
Memory Usage
- Minimal memory overhead beyond problem state
- Each move evaluation is independent (no temporary state)
- Safe for large problems (100K+ objects)
Comparison with Variants
| Move Type | Speed | Thoroughness | Use Case |
|---|---|---|---|
| Single | Medium | Complete | Default choice, thorough search |
| SingleFast | Fast | Partial | Faster convergence, may miss optimal |
| SingleGreedy | Fastest | Greedy | Single-threaded, speed critical |
| SingleRandomBatches | Fast | Sampled | Parallel batching for speed |
Recommendation: Start with Single. Only switch to variants if:
- Speed is critical →
SingleFastorSingleGreedy - Problem is extremely large →
SingleRandomBatcheswith sampling
Troubleshooting
Problem: Single moves too slow
Diagnosis: Large problem (many objects or containers)
Solutions:
- Switch to SingleFast for early termination
- Use SingleRandomStratified with sampling
- Reduce problem size by filtering containers/objects
- Check if moves are being evaluated inefficiently
Problem: Not finding good solutions
Diagnosis: Single moves alone may not escape local optima
Solutions:
- Add Swap for capacity-constrained problems
- Add SingleEndChain for 2-move sequences
- Try multiple solver runs with different random seeds
- Improve initial assignment quality
Problem: Getting stuck in local optimum quickly
Diagnosis: Simple moves can't improve objective further
Solutions:
- Add more powerful move types (Swap, Chain, TripleLoop)
- Check if constraints are blocking improvements
- Verify initial assignment isn't extremely broken
- Review objective function for issues
Related Move Types
Variants:
- SingleFast - Faster with early termination
- SingleGreedy - Greedy single-threaded variant
- SingleRandomBatches - Parallel batched variant
- SingleRandomStratified - Sampled destinations
- SingleColdestStratified - Target cold containers
Complementary:
- Swap - For capacity-constrained problems
- SingleEndChain - For 2-move sequences
- FixedDest - When destination is predetermined
Source Code
- Thrift definition:
interface/thrift/SolverSpecs.thrift:511 - Implementation:
solver/moves/SingleMoveType.h - Tests:
solver/moves/tests/SingleMoveTypeTest.cpp
Next Steps
- Learn about SingleFast for faster convergence
- See Swap for capacity-constrained problems
- Review Move Types Overview for choosing move types
- Check Local Search Solver for using move types