@@ -65,8 +65,13 @@ class TrafficManager:
6565 on a configurable 'mode' ("combine" or "pairwise").
6666 3) Each Demand is associated with a FlowPolicy, which handles how flows
6767 are placed (split across paths, balancing, etc.).
68- 4) Provides methods to place all demands incrementally with optional
69- re-optimization, reset usage, and retrieve flow/usage summaries.
68+ 4) Provides methods to place all demands incrementally with optional
69+ re-optimization, reset usage, and retrieve flow/usage summaries.
70+
71+ Auto rounds semantics:
72+ - placement_rounds="auto" performs up to a small number of fairness passes
73+ (at most 3), with early stop when diminishing returns are detected. Each
74+ pass asks the scheduler to place full leftovers without step splitting.
7075
7176 In particular:
7277 - 'combine' mode:
@@ -172,21 +177,57 @@ def place_all_demands(
172177 raise RuntimeError ("Graph not built yet. Call build_graph() first." )
173178
174179 if isinstance (placement_rounds , str ) and placement_rounds .lower () == "auto" :
175- placement_rounds = self ._estimate_rounds ()
180+ # Simple, reliable auto: up to 3 passes with early stop.
181+ from ngraph .algorithms .base import MIN_FLOW
182+
183+ total_placed = 0.0
184+ max_auto_rounds = 3
185+ for _ in range (max_auto_rounds ):
186+ placed_now = place_demands_round_robin (
187+ graph = self .graph ,
188+ demands = self .demands ,
189+ placement_rounds = 1 ,
190+ reoptimize_after_each_round = False ,
191+ )
192+ total_placed += placed_now
176193
177- # Ensure placement_rounds is an int for range() and arithmetic operations
178- placement_rounds_int = (
179- int (placement_rounds )
180- if isinstance (placement_rounds , str )
181- else placement_rounds
182- )
194+ # Early stops: no progress or negligible leftover
195+ if placed_now < MIN_FLOW :
196+ break
183197
184- total_placed = place_demands_round_robin (
185- graph = self .graph ,
186- demands = self .demands ,
187- placement_rounds = placement_rounds_int ,
188- reoptimize_after_each_round = reoptimize_after_each_round ,
189- )
198+ leftover_total = sum (
199+ max (0.0 , d .volume - d .placed_demand ) for d in self .demands
200+ )
201+ if leftover_total < 0.05 * placed_now :
202+ break
203+
204+ # Fairness check: if served ratios are already close, stop
205+ served = [
206+ (d .placed_demand / d .volume )
207+ for d in self .demands
208+ if d .volume > 0.0 and (d .volume - d .placed_demand ) >= MIN_FLOW
209+ ]
210+ if served :
211+ s_min , s_max = min (served ), max (served )
212+ if s_max <= 0.0 or s_min >= 0.8 * s_max :
213+ break
214+ else :
215+ # Ensure placement_rounds is an int for range() and arithmetic operations
216+ placement_rounds_int = (
217+ int (placement_rounds )
218+ if isinstance (placement_rounds , str )
219+ else placement_rounds
220+ )
221+
222+ if placement_rounds_int <= 0 :
223+ raise ValueError ("placement_rounds must be positive" )
224+
225+ total_placed = place_demands_round_robin (
226+ graph = self .graph ,
227+ demands = self .demands ,
228+ placement_rounds = placement_rounds_int ,
229+ reoptimize_after_each_round = reoptimize_after_each_round ,
230+ )
190231
191232 # Update each TrafficDemand's placed volume
192233 for td in self ._get_traffic_demands ():
0 commit comments