ワンクリックで
rebalancer
Debugging tool for rebalancer constraint violations and move failures using eval_move, list_server_moves, and query_mip CLIs.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Debugging tool for rebalancer constraint violations and move failures using eval_move, list_server_moves, and query_mip CLIs.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | rebalancer |
| description | Debugging tool for rebalancer constraint violations and move failures using eval_move, list_server_moves, and query_mip CLIs. |
| metadata | {"oncalls":["algopt"],"strict":true} |
A CLI for debugging why the rebalancer made (or didn't make) certain moves. It talks to the Rebalancer Explorer backend to evaluate hypothetical moves and inspect constraint/objective impacts.
eval_move works for ALL rebalancer run types — SM, RAS, and any other.
Build all tools once (abbreviated as $EVAL, $LSM, $QM in examples):
EVAL=$(buck build @fbcode//mode/dev fbcode//algopt/rebalancer/fb/tools:eval_move --show-full-output 2>/dev/null | awk '{print $2}')
LSM=$(buck build @fbcode//mode/dev fbcode//algopt/rebalancer/fb/tools:list_server_moves --show-full-output 2>/dev/null | awk '{print $2}')
QM=$(buck build @fbcode//mode/dev fbcode//algopt/rebalancer/fb/tools:query_mip --show-full-output 2>/dev/null | awk '{print $2}')
DIAG=$(buck build @fbcode//mode/dev fbcode//algopt/rebalancer/fb/tools:diagnose_tree --show-full-output 2>/dev/null | awk '{print $2}')
$EVAL --run-id <RUN_ID> --variable <OBJECT_NAME> --container <CONTAINER_NAME>
# List all solver moves to/from a server
$LSM --run-id <RUN_ID> --server <SERVER>
# Look up which server a shard is on (source + destination)
$LSM --run-id <RUN_ID> --shard <SHARD_NAME>
# Compute exact MOVED_DATA utilization for a server (solver moves only)
$LSM --run-id <RUN_ID> --compute-moved-data <SERVER>
# Compute MOVED_DATA including a proposed additional move
$LSM --run-id <RUN_ID> --compute-moved-data <SERVER> --proposed-shard <SHARD>
# Find shards with has_move_in_progress=1 on a server (both src and dst directions)
$QM --run-id <RUN_ID> --server <SERVER>
# Auto-detect blockers and walk their trees
$DIAG --run-id <RUN_ID> --variable <OBJECT_NAME> --container <CONTAINER_NAME>
# Walk a specific expression by name
$DIAG --run-id <RUN_ID> --variable <OBJECT_NAME> --container <CONTAINER_NAME> \
--expression-name "per_server_max_move_"
# Control tree depth (default: 5)
$DIAG --run-id <RUN_ID> --variable <OBJECT_NAME> --container <CONTAINER_NAME> --max-depth 8
Use raw variable/container names exactly as shown in the Explorer UI.
Every command needs a rebalancer run to inspect:
$EVAL --run-id <UUID> --variable <OBJECT_NAME> --container <CONTAINER_NAME>
Get the run ID from the Explorer URL, Scuba, or the user.
| Status | Meaning | Emoji |
|---|---|---|
OK | Satisfied before and after the move | ✅ |
BROKEN | Was OK, now violated — this blocks the move | ❌ |
FIXED | Was violated, now OK — move helps | ✅ |
STILL_BROKEN | Violated before and after | ⚠️ |
A move is blocked if any constraint has status BROKEN.
0 = constraint is satisfied> 0 = constraint is violated (the number = how much it's violated by)Delta = change caused by the move (positive = got worse)improved = value decreased (good — objectives are minimized)regressed = value increased (bad — objective is now violated)unchanged = no change0 = objective is satisfied (not violated)> 0 = objective is violatedDelta > 0 = the move causes a new violation or worsens an existing oneImportant: An objective delta of +1 means the objective becomes violated — it does NOT mean "one more move" or a count increase. Objectives follow the same value semantics as constraints: 0 = satisfied, >0 = violated.
When evaluating a move, the base determines what you're comparing against:
--src-base INITIAL (default): Baseline is the assignment BEFORE the solver ran. Comparing INITIAL vs FINAL+move shows ALL changes including what the solver did. This matches the Explorer UI's default.--src-base FINAL: Baseline is the assignment AFTER the solver ran. Comparing FINAL vs FINAL+move shows ONLY the marginal impact of your move.Always use the default (INITIAL → FINAL) to match what the Explorer shows. Use FINAL → FINAL only when you want to isolate the marginal effect of a single move.
| Verdict | Meaning |
|---|---|
SAFE | Move doesn't break any constraint and doesn't regress objectives |
BLOCKED_BY_CONSTRAINT | Move violates one or more constraints |
OBJECTIVE_REGRESSION | Constraints OK but objectives get worse |
The diagnose_tree tool outputs a nested JSON tree for each blocking expression. Each node contains:
| Field | Meaning |
|---|---|
expressionType | Computation type (e.g., LinearSum, AnyPositive, ObjectLookup) |
sourceValue | Value at the source (baseline) assignment |
destinationValue | Value at the destination (override) assignment |
delta | destinationValue - sourceValue — the change caused by the move |
coefficient | This node's weight in its parent's computation |
properties | Metadata (scope item, dimension, container) — always report when present |
children | Child nodes (only those with ` |
Blame path: Starting from the root, follow the children with the largest |delta| at each level. This traces the primary contributor from the top-level expression down to the leaf-level physical entity.
For deeper understanding of how specs map to tree structures and how to derive construction rationale, see references/diagnosis_guide.md.
Some expressions (like per_server_max_move_) appear as BOTH a constraint AND an objective.
Why this matters: A move can show per_server_max_move_ as STILL_BROKEN on the constraint side (global violation unchanged) but regressed on the objective side (your move causes a violation, going from 0 to 1). Since tuple_index=0 objectives have strict lexicographic priority, any regression there blocks the move.
Verdict = OBJECTIVE_REGRESSION means: no constraint was newly broken, but a high-priority objective got worse. The solver treats this as "not worth doing."
ALWAYS check for moves in progress and avoid assignments when debugging a violation. These are pre-existing conditions that affect constraints and objectives but do NOT appear in the Rebalancer Explorer UI's move diff (INITIAL→FINAL). If the math from solver moves alone doesn't add up, these are the most likely explanation.
has_move_in_progress=1 in the Explorer data table have an active move that hasn't completed yet.MOVED_DATA utilization on both their source and destination servers.getMovesBetweenAssignmentsV2 (INITIAL→FINAL), so they are invisible when counting solver moves.has_move_in_progress >= 1.When the violation math doesn't add up from solver moves alone, always check these hidden factors before concluding there's a bug.
The eval_move tool automatically fetches the spec (configuration) for each blocking expression. The "specs" section in the output contains the parsed spec JSON for each regressed objective and each BROKEN constraint.
Always start with the spec when explaining a violation. The spec tells you exactly what the expression is configured to enforce — its partition, dimension, limits, and filters. This is the source of truth.
An expression that is both an objective and a constraint will have both entries:
{
"specs": {
"per_server_max_move_": {
"objective": {
"type": "objective",
"weight": 1.0,
"tuple_index": 0,
"spec": { ... }
},
"constraint": {
"type": "constraint",
"invalid_cost": ...,
"invalid_state": ...,
"policy": "...",
"spec": { ... }
}
}
}
}
An expression that is only a constraint or only an objective will have a single entry:
{
"specs": {
"BALANCE_LOADlower_bound_region_gbl_flash_storage": {
"type": "constraint",
"invalid_cost": ...,
"invalid_state": ...,
"policy": "...",
"spec": { ... }
}
}
}
For a complete catalog of all 51 spec types with their thrift definitions, enum values, and constraint-vs-goal usage, see references/spec_catalog.md.
| Spec Type | Key Fields | What It Enforces |
|---|---|---|
CapacitySpec | scope, dimension, definition, bound, limit | Bounds utilization on scope items. The definition and bound fields control what is measured and in which direction — read them carefully. |
GroupMoveLimitSpec | partitionName, dimension, limit, sourceScopeItemsAffectingLimitFilter, destinationScopeItemsAffectingLimitFilter | Limits how many objects can move within each group of a partition |
BalanceLoadSpec | scope, dimension, lowerBound, upperBound | Keeps load balanced across scope items within bounds |
definition and bound FieldsThe CapacitySpec has two critical enum fields that determine its semantics. Always read both before interpreting the spec.
definition (what is measured):
| Value | Name | Meaning |
|---|---|---|
| 1 | AFTER | Utilization after all moves are applied |
| 2 | DURING_AND_AFTER | Utilization during and after moves |
| 3 | DURING | Utilization only during moves (transient state) |
| 4 | DOUBLE_DURING_AND_AFTER | Double utilization during and after |
| 5 | DOUBLE_DURING | Double utilization during moves |
| 6 | NEW | Utilization from newly assigned objects only |
| 7 | OLD | Utilization from objects in their original assignment |
| 8 | MOVED_DATA | Utilization from only the data that moved — affects BOTH the source and destination containers (not just the destination) |
bound (direction of the limit):
| Value | Name | Meaning |
|---|---|---|
| 1 | MAX | Upper bound — utilization must not exceed the limit |
| 2 | MIN | Lower bound — utilization must not fall below the limit |
Example: per_server_max_move_
definition=MOVED_DATA + bound=MAX + dimension=__shard___count__ + scope=__server__MOVED_DATA charges both the source and destination servers. Moving a shard from server A to server B increases the moved-data count on both A and B.MOVED_DATA includes moves in progress (pre-existing moves that haven't completed yet). These are shards with has_move_in_progress=1 in the Explorer data table. They contribute to the moved-data count but do NOT appear in the INITIAL→FINAL move diff. Always check for moves in progress when the math doesn't add up.scopeItemLimits={"<server_hash_1>": 20, "<server_hash_2>": 19}, each server has its own limit. The violation formula is max(0, utilization - limit) — the spec only breaks when utilization strictly exceeds the limit, not when it equals it.When explaining a violation:
When analyzing eval_move output, ALWAYS check objectives before constraints, regardless of the verdict. The solver uses lexicographic priority, so a tuple_index=0 objective regression is the most important blocker — even if there are also BROKEN constraints.
Priority order for identifying the primary blocker:
per_server_max_move_) — highest priority, always report firstBROKEN constraints — report after objectivesWhy this matters: A move may show multiple BROKEN constraints AND an objective regression. Even if you fixed all the BROKEN constraints, the move would STILL be blocked by the tuple_index=0 objective regression. Reporting BROKEN constraints as the primary blocker is misleading — the objective regression is the dominant reason the solver rejects the move.
$EVAL --run-id $RUN_ID --variable "$VARIABLE" --container "$CONTAINER"
direction: regressed. If any have tuple_index: 0, this is the primary blocker due to strict lexicographic priority.BROKEN.SAFE → the move is not blocked. If the user is asking why the solver didn't make this move, follow Recipe 4 ("Why didn't the solver make this SAFE move?")."specs" section of the output):
per_server_max_move_ fires → follow Recipe 2 in full (look up source server, list solver moves, query MIP, compute MOVED_DATA, build the math)$LSM --shard to find the source server, then investigate utilization on both sidesexisting_utilization + proposed_change vs limit, with concrete numbersper_server_max_move_ (tuple_index=0 objective). According to its spec, this is a CapacitySpec with definition=MOVED_DATA and bound=MAX on scope=__server__ with dimension=__shard___count__. The source server [X] has limit=[N]. Solver moves involving this server: [M1] from + [M2] to = [M] total. Moves in progress: [P]. Total MOVED_DATA = [M+P] = [T]. Your proposed move would increase this to [T+1], which exceeds the limit of [N]. Additionally, 5 BALANCE_LOAD constraints would break (secondary)."This is a move-limiting expression that uses a CapacitySpec with definition=MOVED_DATA and bound=MAX.
What the spec means: It's a CapacitySpec (not a GroupMoveLimitSpec) with:
definition=MOVED_DATA — measures only the data that has been moved (not the total load)bound=MAX — upper bound (must not exceed the limit)dimension=__shard___count__ — measured in shard count unitsscope=__server__ — applied per serverscopeItemLimits — per-server limits (e.g., 20 for a given server)Key insight: per_server_max_move_ is registered as BOTH a constraint AND an objective.
STILL_BROKEN (global, unchanged) but the objective as regressed (going from 0 to 1, meaning it becomes violated)MOVED_DATA formula: MOVED_DATA = afterExpr + initialExpr - 2 * stayedExpr. In practice, for shard count, this equals the total number of shards that moved to or from a server (solver moves + moves in progress).
Debugging steps — MANDATORY deep investigation:
You MUST run ALL of these steps. Do NOT stop at step 3 and guess — always get the actual numbers.
Run eval_move to get the verdict, specs, and the configured limit:
$EVAL --run-id $RUN_ID --variable "$OBJECT" --container "$CONTAINER"
Extract the scopeItemLimits for both source and destination servers from the spec.
Look up the shard's source server using list_server_moves --shard:
$LSM --run-id $RUN_ID --shard "$OBJECT"
This returns the shard's src.__server__ and dst.__server__, giving you the source server hash.
For BOTH source and destination servers, run these three commands:
a. List solver moves (moves in the INITIAL→FINAL diff):
$LSM --run-id $RUN_ID --server <SERVER>
This shows moves_to_server, moves_from_server, and total_moves_involving_server. These are the solver's planned moves.
b. Query moves in progress (pre-existing moves not in the solver diff):
$QM --run-id $RUN_ID --server <SERVER>
This shows shards with has_move_in_progress=1 both FROM and TO this server. These contribute to MOVED_DATA but are invisible in the INITIAL→FINAL move list.
c. Compute MOVED_DATA utilization (exact accounting including the proposed move):
$LSM --run-id $RUN_ID --compute-moved-data <SERVER> --proposed-shard "$OBJECT"
This outputs total_moved_data (solver moves only) and total_with_proposed (solver + your move). Note: this does NOT include moves in progress — you must add those from step 3b.
Build the complete math for each server:
Total MOVED_DATA = solver_moved_data + moves_in_progress_count + proposed_move
Violation = max(0, Total_MOVED_DATA - scopeItemLimit)
For each server, present a table like:
| Component | Count | Source |
|---|---|---|
| Solver moves (from server) | X | list_server_moves |
| Solver moves (to server) | Y | list_server_moves |
| Moves in progress (from) | P1 | query_mip |
| Moves in progress (to) | P2 | query_mip |
| Subtotal (existing) | X+Y+P1+P2 | |
| Proposed move | +1 | your move |
| Total MOVED_DATA | X+Y+P1+P2+1 | |
| Limit | L | scopeItemLimits |
| Over limit by | max(0, total-L) |
Identify which server causes the violation: The violation comes from whichever server's total MOVED_DATA exceeds its scopeItemLimit. It may be the source, destination, or both.
Present the complete explanation with all numbers:
The primary blocker is `per_server_max_move_` (tuple_index=0 objective).
Source server [SRC_HASH] (limit=[N]):
- Solver moves: [X] from + [Y] to = [Z] total
- Moves in progress: [P] shards
- Current MOVED_DATA: [Z+P]
- With proposed move: [Z+P+1]
- Status: [OVER/UNDER] limit by [diff]
Destination server [DST_HASH] (limit=[M]):
- Solver moves: [A] from + [B] to = [C] total
- Moves in progress: [Q] shards
- Current MOVED_DATA: [C+Q]
- With proposed move: [C+Q+1]
- Status: [OVER/UNDER] limit by [diff]
The violation comes from [SOURCE/DESTINATION/BOTH] server(s).
eval_move for each candidate:
$EVAL --run-id $RUN_ID --variable "$OBJECT" --container "$CONTAINER"
BROKENrack_capacity_* — rack is fullsubtype_* — object type doesn't match what's neededper_server_max_move_* — too many moves alreadyfailure_domain_* — would put too many objects in one failure domainSometimes eval_move with the default INITIAL → FINAL base returns SAFE, meaning the move is not blocked — yet the solver didn't include it. This happens because the solver is an optimizer: it only makes moves that improve objectives. A SAFE verdict means the move wouldn't break anything, but the solver may still skip it if the move provides no marginal benefit.
IMPORTANT: When the INITIAL → FINAL eval returns SAFE and the user is asking why the solver didn't make the move, you MUST proceed with these steps. Do NOT just report the move as SAFE and stop.
Why INITIAL → FINAL can be misleading for SAFE moves:
The INITIAL → FINAL comparison shows what would happen if this move were added on top of the solver's FINAL solution. STILL_BROKEN constraints with large negative deltas may appear to show huge improvements — but those improvements were already achieved by other moves the solver made. This move is being falsely credited for work that other moves did. To understand the true marginal impact, you MUST run the FINAL → FINAL comparison.
Debugging steps:
Confirm SAFE with INITIAL → FINAL (default eval_move):
$EVAL --run-id $RUN_ID --variable "$OBJECT" --container "$CONTAINER"
If verdict is not SAFE, use Recipe 1 or 2 instead.
Run FINAL → FINAL to see the true marginal impact:
$EVAL --run-id $RUN_ID --variable "$OBJECT" --container "$CONTAINER" --src-base FINAL --dst-base FINAL
This compares the solver's final assignment with vs without your move — isolating the pure marginal effect.
Analyze the marginal impact:
Present the explanation comparing both perspectives:
INITIAL → FINAL verdict: SAFE
This comparison shows large improvements, but these are misleading — they
were already achieved by other solver moves.
FINAL → FINAL verdict: SAFE
Marginal impact of this specific move on top of the solver's solution:
- Improved objectives: [list, or "none"]
- Regressed objectives: [list, or "none"]
- Changed constraints: [list, or "none"]
The solver did not make this move because [reason]:
- "it provides zero marginal improvement to any objective"
- "the marginal improvement is extremely small (delta = X) and not worth the move cost"
- "it improves [obj_A] but regresses [obj_B] which has higher priority"
- "the solver likely ran out of move budget or time before reaching this move"
Common root causes for SAFE-but-not-made:
| Scenario | Marginal Objectives | Marginal Constraints | Explanation |
|---|---|---|---|
| Zero benefit | No change | No change | Move is neutral — solver has no reason to make it |
| Tiny benefit | Improved by < 0.01 | Minor improvement | Improvement too small to justify a move |
| Trade-off | Mixed improved/regressed | Mixed | Net effect is negative or neutral |
| Budget exhausted | Improved | No new violations | Solver ran out of moves or time; this move was lower priority |
| Precision truncation | No change (but expected) | Near-zero change | Improvement exists but falls below precision tolerance |
Precision truncation: The solver uses 1e-10 default precision. If improvements fall below this, they're treated as zero. Additionally, eval_move hides objectives with |delta| < 1e-9. See references/precision_tolerance_guide.md for the full precision comparison logic, verification commands, calculation templates, and recommended actions table.
Key pattern: "zero objectives but constraint improvements visible" — FINAL → FINAL shows 0 objectives but constraint deltas (e.g., small negative values) → sub-1e-9 objective improvements are hidden. Recommend 1e-16 precision.
MANDATORY: When FINAL → FINAL shows near-zero improvement, compute the required precision tolerance using the formula in references/precision_tolerance_guide.md. Do NOT suggest arbitrary values.
Diagnostic checklist:
1e-16)1e-16 precision to confirm the diagnosis empirically. Do NOT just report the precision hypothesis without testing it. See step 5 below.Step 5: Verify precision truncation with standalone solver
When you diagnose precision truncation (zero objectives but constraint improvements visible), you MUST run the standalone solver to confirm:
# Build the standalone solver once (abbreviated as $SOLVER in examples)
SOLVER="buck2 run @fbcode//mode/opt fbcode//algopt/rebalancer/interface/standalone:standalone_solver --"
# Run with 1e-16 precision
$SOLVER --run-id $RUN_ID \
--precision_tolerance_absolute 1e-16 \
--precision_tolerance_relative 1e-16 \
2>&1 | tee /tmp/solver_1e16.log
# Run with default precision for comparison
$SOLVER --run-id $RUN_ID \
2>&1 | tee /tmp/solver_default.log
# Compare move counts
grep "moves applied" /tmp/solver_1e16.log /tmp/solver_default.log
How to interpret results:
1e-16 produces significantly more moves → precision truncation confirmed. Report the actual move counts.IMPORTANT: Always run BOTH solvers and report the actual move counts to the user. Do NOT skip this step or substitute it with theoretical analysis. The standalone solver is the ground truth.
Note: Large problems (hundreds of thousands of objects) can take up to 7200s to solve. Run both solver commands in parallel to save time. For quick verification, compare Stage-0 move counts.
See references/precision_tolerance_guide.md for the full precision comparison logic, calculation templates, and recommended actions table.
Scenario: A GROUP_ROUTING move type is configured to place objects (e.g., tenant replicas) in all demand regions, but the solver only places in a subset. The user expects placement in all regions except those where capacity is explicitly unavailable.
Common root causes:
GroupRoutingMoveType generates a single MoveSet containing moves to ALL required destinations. If ANY destination violates a hard constraint (e.g., capacity), the entire MoveSet is rejected — including valid placements to other regions.non_accepting_containers constraint), which prevents any placement there.Debugging steps:
eval_move for a manual placement of one object to one missing region. If it shows constraint violations, identify which constraint blocks it.Scenario: A user manually tests a move in Rebalancer Explorer (FINAL → FINAL) and sees it improves soft threshold or other objectives. But the solver didn't make this move.
Common root causes:
1e-10 default precision). See Recipe 4 for detailed precision analysis.sample_size vs server_count.Debugging steps:
Scenario: A user observes a move that seems unnecessary or counterproductive. They want to understand why the solver chose to make it.
Common root causes:
1e-7). The solver accepts any improving move, even tiny ones.Debugging steps:
eval_move to evaluate the reverse move (undo) to see which objective regresses when the move is undone:
$EVAL --run-id $RUN_ID --variable "$OBJECT" --container "$ORIGINAL_CONTAINER"
1e-6).When to use: After eval_move identifies a blocking expression but the reason for its value is unclear from the spec alone. The expression tree reveals the internal computation hierarchy — which sub-expressions contribute, their coefficients, and their individual deltas.
Debugging steps:
Run eval_move to get the verdict, blocking expression name, AND its spec JSON:
$EVAL --run-id $RUN_ID --variable "$OBJECT" --container "$CONTAINER"
Note the blocking expression name and its spec.
Run diagnose_tree to get the expression tree:
$DIAG --run-id $RUN_ID --variable "$OBJECT" --container "$CONTAINER" \
--expression-name "<BLOCKING_EXPRESSION_NAME>"
If you want all blockers, omit --expression-name.
Follow the blame path: Starting from the root node, at each level find the child with the largest |delta|. This traces the primary contributor through the tree.
At each level, read expressionType to understand the computation:
LinearSum: value = constant + Σ(coefficient × child). The child with the largest |coefficient × child_delta| is the primary contributor.AnyPositive: the child that crossed from ≤0 to >0 caused the violation.Max: the child that became the new maximum is the culprit.BinaryOperation: compare both operands' deltas.Power/Square: delta is amplified non-linearly.Correlate the tree with the spec: Read the spec JSON fields (scope, dimension, bound, definition, limits, etc.) and use the generic materialization rules from references/diagnosis_guide.md to reason about WHY each tree node exists:
LinearSum = summed violations, AnyPositive = conjunction)name fields for scope item identifiersPower = non-linear penalty, BinaryOperation = difference from limit)ObjectLookup nodes correspond to the dimension and scope fieldsAt leaf nodes, properties and name reveal the physical entity (which scope item, dimension, container).
Explain in plain language connecting: spec intent → tree structure → leaf-level cause.
The expression `balance_load_cpu` (BalanceLoadSpec, scope=region, dimension=cpu) has a
LinearSum root summing per-region violations. The largest delta comes from region=us-east
(child delta=+0.15). That child is a BinaryOperation(utilization - upperBound), where
utilization increased from 0.82 to 0.97 (ObjectLookup leaf: your shard added +0.15 cpu
to us-east). The upper bound is 0.90, so the violation is 0.97 - 0.90 = 0.07.
Scenario: A solver that was working well suddenly produces significantly fewer moves. The regression correlates with a specific date or package version.
Debugging steps:
rebalancer.packer releases on Conveyor).--packer-version <OLD_VERSION>
| Symptom | Cause | Fix |
|---|---|---|
| "Sandbox not loaded" / timeout | Run ID expired from Manifold | Find a fresh run ID from Explorer or Scuba |
| "Variable not found" | Object name format wrong | Use exact names from the Explorer UI |
| "Container not found" | Container name format wrong | Use exact names from the Explorer UI |
Verdict is OBJECTIVE_REGRESSION not BLOCKED_BY_CONSTRAINT | Expression is both a constraint AND objective; the objective regresses | Check the objectives list, not just constraints. See "Constraint vs Objective Duality" section |
evaluate shows SAFE but system says blocked | Different run or stale data | Verify run ID matches what the system used. Check --src-base and --dst-base |
| Slow sandbox loading | Large problem instance | Increase timeout: --timeout 300 |
| Solver moves objects to over-capacity containers | Load is tiny relative to capacity, so floating-point comparison can't detect the overload (e.g., load=105 vs capacity=5.3e12) | Mark over-capacity containers as non-accepting in the problem setup |
| No moves despite unassigned objects and apparent improvements in Explorer | One object has extreme dimension values (e.g., CPU >1e15) dwarfing all others (1e5). Normalization makes smaller objects insignificant. | Remove the extreme object, or separate its goal into a different tuple. Sort by dimension in Explorer to find outliers. |
| Constraint appears violated in production but Explorer shows it improving | Constraint was initially broken. Default policy splits it into an objective (minimize violation) + constraint (don't worsen). The objective is improving but can't reach zero due to other constraints or time limits. | Check if the constraint has both an objective and constraint entry in Explorer. The initial violation is expected — look at the objective's improvement instead. See ConstraintPolicy in Types.thrift. |
| Solver proposes many moves internally but applies zero | Numerical instability — objective values span too many orders of magnitude (e.g., 1e16). MIP solver proposes moves but rebalancer rejects them as numerical artifacts. | Rescale dimension values so the coefficient range is < 1e9 (ideally < 1e6). Check solver logs for coefficient range warnings. |
Balance spec upperBound seems ineffective | Average utilization is very low, so even a high relative bound maps to a low absolute threshold that many containers already exceed. E.g., avg=0.22, bound=1.9 → threshold=0.42. | Increase the bound further, or increase the move budget. Check actual average utilization to understand the threshold. |
| Problem declared infeasible intermittently | MIP solver (Gurobi/Xpress) is sensitive to seed and tolerance values. Tight tolerances can put the problem "on the edge". | Try different solver seeds, upgrade solver version (newer versions have better multi-objective handling), or relax tolerances slightly. |
Variable and container names depend on the run type. Always use the exact names shown in the Explorer UI.
| Run Type | Object (variable) | Container | Example |
|---|---|---|---|
| SM (shard manager) | <shard_id> | <server_hash> | 12345:0,1234500100-0,1234500100:1 → -123456789 |
| Other | Varies | Varies | Check Explorer UI for exact names |
| Path | Purpose |
|---|---|
algopt/rebalancer/fb/tools/eval_move.py | Move evaluator (works for ALL run types) |
algopt/rebalancer/fb/tools/list_server_moves.py | List solver moves to/from a server, lookup shard source/dest, compute MOVED_DATA |
algopt/rebalancer/fb/tools/query_mip.py | Query shards with moves in progress (has_move_in_progress=1) on a server |
algopt/rebalancer/fb/tools/diagnose_tree.py | Walk expression trees for blocking expressions via getTreeNodeV2 |
rebalancer/explorer/if/explorer.thrift | Thrift API definition |
| When to use | Reference |
|---|---|
| Plan for rebalancer debugging skill development. | PLAN.md |
Performance testing CLI for the rebalancer solver (algopt/rebalancer/). Use when benchmarking solver performance, comparing commits for regressions, profiling solver execution, running A/B tests, or querying the rebalancer_runs Scuba table. Covers local, Skycastle (distributed), and ServiceLab (isolated hardware) execution environments. Also handles replaying saved solver configs and managing predefined run collections like the pareto golden set. Only for the rebalancer project — not for general ServiceLab benchmarks, non-rebalancer services, or functional correctness testing.
Use when GitHub Actions CI is failing, when investigating build or test failures on a branch or PR, when the user asks about CI status, or when you need to diagnose why a workflow failed, or when you need to examine GHA build logs (e.g. sccache stats, cache usage, build timing). Triggers on keywords like "CI", "GHA", "GitHub Actions", "build failure", "tests failing", "pipeline broken", "build logs", "sccache", "cache stats", "build performance".