GreedyGroupToScopeItem
Move Type: Group Complexity: O(G × S × K) - greedy group placement Primary Use: Place entire groups with unique container constraint
Move entire groups of objects to scope items where each object gets a unique container. Enforces anti-affinity within the group.
Overview
GreedyGroupToScopeItem (also known as GREEDY_GROUP_TO_SCOPE_ITEM) is a specialized move type that places all objects in a group to containers within a single scope item, with the critical constraint that each object must go to a different container.
This enforces anti-affinity: if you have 5 objects in a group, they will be placed on 5 different containers within the chosen scope item.
Use when:
- Need anti-affinity within groups
- Each object in group requires unique container
- Placing replicas on different machines
- Task placement where tasks need different hosts
- Failure domain isolation within groups
Avoid when:
- Objects can share containers (use GroupMoveWithHintStrategies instead)
- Don't have enough containers per scope item
- No group structure
- Simple colocation without anti-affinity
Quick Example
- Python
- C++
# Move entire job to one datacenter, each task on different host
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
GreedyGroupToScopeItemMoveTypeSpec(
groupMovesPartition="job",
scopeItemMovesScope="datacenter",
),
]
)
)
)
void quickExample() {
// Move entire job to one datacenter, each task on different host
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("task");
solver.setContainerName("host");
// Setup problem with job scattered across datacenters
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host1", {"task0"}},
{"host2", {"task1"}},
{"host3", {"task2"}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 1.0},
{"task1", 1.0},
{"task2", 1.0},
});
// Define partition with jobs
std::unordered_map<std::string, std::vector<std::string>> jobs = {
{"job0", {"task0", "task1", "task2"}},
};
solver.addPartition("job", jobs);
// Define scope with datacenters
std::map<std::string, std::string> dcScope = {
{"host1", "dc1"},
{"host2", "dc1"},
{"host3", "dc1"},
};
solver.addScope("datacenter", dcScope);
LocalSearchSolverSpec solverSpec;
GreedyGroupToScopeItemMoveTypeSpec greedySpec;
greedySpec.groupMovesPartition() = "job";
greedySpec.scopeItemMovesScope() = "datacenter";
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().greedyGroupToScopeItemMoveTypeSpec() =
greedySpec;
solver.addSolver(solverSpec);
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: " << std::fixed << std::setprecision(4)
<< *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 |
|---|---|---|---|---|
groupMovesPartition | string | Yes | null | Partition defining groups |
scopeItemMovesScope | string | Yes | null | Scope to move groups into |
nSampleSetsToExplore | int | No | 2 | Sample sets per scope item |
Parameter Details
groupMovesPartition:
- The partition name defining the groups
- Each group will be moved as a unit
- All objects in a group move together to the same scope item
- But each object goes to a different container within that scope item
scopeItemMovesScope:
- The scope whose scope items will receive the groups
- Example: "rack", "datacenter", "availability_zone"
- Only scope items with enough containers are considered
nSampleSetsToExplore:
- Number of random container sets to try per scope item
- Higher values = better quality but slower
- For each scope item, generate k random sets of containers
- Each set has as many containers as objects in the group
How It Works
For each group in the partition:
- Identify candidate scope items: Find scope items in the target scope with at least N containers (where N = group size)
- For each scope item: Generate
nSampleSetsToExplorerandom sets of containers- Each set has exactly N unique containers
- Random sampling for diversity
- Evaluate each set: Test moving the group's objects to each container set
- Select best: Choose the move that improves objective most
- Repeat: Process all groups
Visual Example
Group: job0 with 3 tasks [task0, task1, task2]
Scope: datacenter
ScopeItems:
- dc1: 5 hosts [host1, host2, host3, host4, host5]
- dc2: 3 hosts [host6, host7, host8]
- dc3: 2 hosts [host9, host10] ✗ Skip (not enough containers)
For dc1 (nSampleSetsToExplore=2):
Sample Set 1: [host1, host3, host5]
task0 → host1
task1 → host3
task2 → host5
Sample Set 2: [host2, host4, host5]
task0 → host2
task1 → host4
task2 → host5
For dc2 (nSampleSetsToExplore=2):
Sample Set 1: [host6, host7, host8]
task0 → host6
task1 → host7
task2 → host8
Sample Set 2: [host6, host8, host7]
task0 → host6
task1 → host8
task2 → host7
Evaluate all 4 sample sets, choose best
Complexity
Per group: O(S × K)
Where:
- S = number of scope items in target scope
- K =
nSampleSetsToExplore
Total: O(G × S × K)
Where:
- G = number of groups in partition
Example calculation:
- Groups (G): 100 jobs
- Scope items (S): 10 datacenters
- Sample sets (K): 2
- Total evaluations: 100 × 10 × 2 = 2,000
Usage Patterns
Replica Anti-Affinity
Place replicas on different machines within same rack:
- Python
- C++
# Place all replicas of a shard in same rack, but on different machines
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
GreedyGroupToScopeItemMoveTypeSpec(
groupMovesPartition="shard",
scopeItemMovesScope="rack",
nSampleSetsToExplore=2,
),
]
)
)
)
void replicaAntiaffinity() {
// Place all replicas of a shard in same rack, but on different machines
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("replica");
solver.setContainerName("machine");
// Setup with replicas scattered across racks
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"machine1", {"shard1_primary"}},
{"machine2", {"shard2_primary"}},
{"machine3", {}},
{"machine4", {"shard1_replica1", "shard2_replica1"}},
{"machine5", {"shard1_replica2"}},
{"machine6", {"shard2_replica2"}},
});
solver.addObjectDimension(
"size",
std::map<std::string, double>{
{"shard1_primary", 1.0},
{"shard1_replica1", 1.0},
{"shard1_replica2", 1.0},
{"shard2_primary", 1.0},
{"shard2_replica1", 1.0},
{"shard2_replica2", 1.0},
});
// Define shard replicas
std::unordered_map<std::string, std::vector<std::string>> shards = {
{"shard1", {"shard1_primary", "shard1_replica1", "shard1_replica2"}},
{"shard2", {"shard2_primary", "shard2_replica1", "shard2_replica2"}},
};
solver.addPartition("shard", std::move(shards));
// Define rack scope
std::map<std::string, std::string> rackScope = {
{"machine1", "rack1"},
{"machine2", "rack1"},
{"machine3", "rack1"},
{"machine4", "rack2"},
{"machine5", "rack2"},
{"machine6", "rack2"},
};
solver.addScope("rack", rackScope);
LocalSearchSolverSpec solverSpec;
GreedyGroupToScopeItemMoveTypeSpec greedySpec;
greedySpec.groupMovesPartition() = "shard";
greedySpec.scopeItemMovesScope() = "rack";
greedySpec.nSampleSetsToExplore() = 2;
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().greedyGroupToScopeItemMoveTypeSpec() =
greedySpec;
solver.addSolver(solverSpec);
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-size";
balanceSpec.scope() = "machine";
balanceSpec.dimension() = "size";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << std::fixed << std::setprecision(4)
<< *solution.initialObjective()->value() << "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Job Task Placement
Place all tasks of a job in same datacenter on different hosts:
- Python
- C++
# Keep job tasks together in one datacenter, spread across hosts
solver = ProblemSolver(service_name="example", service_scope="test")
solver.setObjectName("task")
solver.setContainerName("host")
# Define jobs
solver.addPartition(
"job",
{
"webserver": ["web_task1", "web_task2", "web_task3"],
"database": ["db_task1", "db_task2"],
},
)
# Define datacenter scope
solver.addScope(
"datacenter",
{
"host1": "us-east",
"host2": "us-east",
"host3": "us-east",
"host4": "us-west",
"host5": "us-west",
"host6": "us-west",
},
)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
GreedyGroupToScopeItemMoveTypeSpec(
groupMovesPartition="job",
scopeItemMovesScope="datacenter",
),
]
)
)
)
void jobPlacement() {
// Keep job tasks together in one datacenter, spread across hosts
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("task");
solver.setContainerName("host");
// Define jobs
std::unordered_map<std::string, std::vector<std::string>> jobs = {
{"webserver", {"web_task1", "web_task2", "web_task3"}},
{"database", {"db_task1", "db_task2"}},
};
solver.addPartition("job", std::move(jobs));
// Define datacenter scope
std::map<std::string, std::string> dcScope = {
{"host1", "us-east"},
{"host2", "us-east"},
{"host3", "us-east"},
{"host4", "us-west"},
{"host5", "us-west"},
{"host6", "us-west"},
};
solver.addScope("datacenter", dcScope);
LocalSearchSolverSpec solverSpec;
GreedyGroupToScopeItemMoveTypeSpec greedySpec;
greedySpec.groupMovesPartition() = "job";
greedySpec.scopeItemMovesScope() = "datacenter";
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().greedyGroupToScopeItemMoveTypeSpec() =
greedySpec;
solver.addSolver(solverSpec);
}
Higher Sampling
Increase sampling for better quality:
- Python
- C++
# Explore more container sets for better placement quality
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
GreedyGroupToScopeItemMoveTypeSpec(
groupMovesPartition="service",
scopeItemMovesScope="zone",
nSampleSetsToExplore=10, # Higher sampling for better quality
),
]
)
)
)
void higherSampling() {
// Explore more container sets for better placement quality
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
std::unordered_map<std::string, std::vector<std::string>> services = {
{"service1", {"instance1", "instance2", "instance3"}},
};
solver.addPartition("service", std::move(services));
std::map<std::string, std::string> zoneScope = {
{"node1", "zone1"},
{"node2", "zone1"},
{"node3", "zone1"},
};
solver.addScope("zone", zoneScope);
LocalSearchSolverSpec solverSpec;
GreedyGroupToScopeItemMoveTypeSpec greedySpec;
greedySpec.groupMovesPartition() = "service";
greedySpec.scopeItemMovesScope() = "zone";
greedySpec.nSampleSetsToExplore() = 10; // Higher sampling for better quality
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().greedyGroupToScopeItemMoveTypeSpec() =
greedySpec;
solver.addSolver(solverSpec);
}
Performance Characteristics
When Does It Help?
GreedyGroupToScopeItem helps when:
- Anti-affinity required: Objects in group must be on different containers
- Failure domain isolation: Spread replicas/tasks across machines
- Rack/datacenter placement: Group together in scope but spread across containers
- Job scheduling: Tasks on different hosts within cluster
- High availability: Ensure group members isolated
GreedyGroupToScopeItem does NOT help when:
- Objects can share containers: Use other group move types
- Not enough containers: Scope items have fewer containers than group size
- No group structure: Objects not organized in groups
- Simple placement: Don't need anti-affinity enforcement
Sampling Trade-off
| nSampleSetsToExplore | Quality | Speed | Use Case |
|---|---|---|---|
| 1 | Lower | Fast | Quick placement |
| 2 | Good | Moderate | Default balanced |
| 5 | Better | Slower | High quality needed |
| 10 | Best | Slow | Critical placement |
Comparison with Alternatives
| Move Type | Anti-Affinity | Constraint | Use Case |
|---|---|---|---|
| ColocateGroups | No | Groups in same scope item | Colocation |
| GreedyGroupToScopeItem | Yes | Unique containers per object | Anti-affinity |
| GroupMoveWithHintStrategies | Optional | Strategy-based | Large-scale with hints |
Troubleshooting
Problem: No moves found
Diagnosis: Scope items don't have enough containers
Solutions:
- Check group sizes vs containers per scope item
- Ensure scope items have ≥ N containers (N = max group size)
- Review partition definition
- May need to use different scope with more containers
Problem: Groups not moving together
Diagnosis: Partition or scope configuration issue
Solutions:
- Verify
groupMovesPartitionis correct - Check
scopeItemMovesScopeis defined - Ensure objects are in the partition groups
- Review scope definition and membership
Problem: Poor solution quality
Diagnosis: Not enough sampling diversity
Solutions:
- Increase
nSampleSetsToExplore(try 5 or 10) - Check if scope items are balanced
- May need additional move types
- Review objective function
Problem: Too slow
Diagnosis: Too many groups or sample sets
Solutions:
- Reduce
nSampleSetsToExploreto 1 - Check number of groups (may be very large)
- Consider batching groups
- May need different move type for this scale
When to Use GreedyGroupToScopeItem
DO use when:
- Need anti-affinity within groups
- Each object requires unique container
- Placing replicas on different machines
- Task scheduling with host diversity
- Failure domain isolation
DO NOT use when:
- Objects can share containers
- Insufficient containers per scope item
- No group structure
- Don't need anti-affinity
- Looking for colocation without spreading
Related Move Types
Group-based alternatives:
- ColocateGroups - Collocate without anti-affinity
- GroupMoveWithHintStrategies - Strategy-based group placement
- GroupRouting - Routing-aware group placement
General alternatives:
- Single - Individual object moves
- SingleGreedy - Greedy single moves
Source Code
- Thrift definition:
interface/thrift/SolverSpecs.thrift:683 - Implementation:
solver/moves/GreedyGroupToScopeItemMoveType.h - Tests:
interface/tests/GreedyGroupToScopeItemMoveTypeTest.cpp
Next Steps
- Learn about ColocateGroups for group colocation
- Try GroupMoveWithHintStrategies for large-scale problems
- Review Move Types Overview for choosing move types
- See anti-affinity constraint documentation
Notes
⚠️ Unique Container Constraint: This move type enforces that each object in a group goes to a different container within the chosen scope item. This is a hard constraint - scope items without enough containers will be skipped.