Skip to content

Commit 442c205

Browse files
khatwanimohitGoogle-ML-Automation
authored andcommitted
PR AI-Hypercomputer#4219: [Diloco] add DCN bandwidth throttling options
Imported from GitHub PR AI-Hypercomputer#4219 # Description This change introduces programmatic traffic control ( tc ) bandwidth throttling support in MaxText and includes a dedicated DCN bandwidth microbenchmark script. This allows simulated network environment testing (e.g. limiting inter-slice DCN bandwidth) during multi-slice training. # Key Changes • Centralized Throttling Utilities: Implemented apply_dcn_throttling and cleanup_dcn_throttling inside train_utils.py using the Linux traffic control ( tc ) utility with a Token Bucket Filter ( tbf ) queuing discipline. • Training Integration: Integrated DCN throttling within train.py's main training loop, applying configured traffic rules at startup and cleaning them up in the finally block of train_loop . • Config Specifications: Introduced default configurations in base.yml and types.py for the following variables: - dcn_bandwidth_limit - dcn_bandwidth_burst - dcn_bandwidth_latency - dcn_bandwidth_interface • Microbenchmarking Script: Added scripts/dcn_bandwidth_test.py which creates a hybrid multi-slice device mesh and runs collective shard-mapped psum operations to measure and print latency/achieved DCN bandwidth under throttling constraints. If the change fixes a bug or a Github issue, please include a link, e.g.,: *Notice 1:* Once all tests pass, the "pull ready" label will automatically be assigned. This label is used for administrative purposes. Please do not add it manually. *Notice 2:* For external contributions, our settings currently require an approval from a MaxText maintainer to trigger CI tests. # Tests Added tests in b/526650570 # Checklist Before submitting this PR, please make sure (put X in square brackets): - [x] I have performed a self-review of my code. For an optional AI review, add the `gemini-review` label. - [x] I have necessary comments in my code, particularly in hard-to-understand areas. - [x] I have run end-to-end tests tests and provided workload links above if applicable. - [x] I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in [our documentation](https://maxtext.readthedocs.io/en/latest/development.html#adding-new-documentation-files). Copybara import of the project: -- 1bc6b5f by Mohit Khatwani <mohitkhatwani@google.com>: dcn throttling changes Merging this change closes AI-Hypercomputer#4219 COPYBARA_INTEGRATE_REVIEW=AI-Hypercomputer#4219 from AI-Hypercomputer:mohit/dcn_throttling 1bc6b5f PiperOrigin-RevId: 937544685
1 parent 425a33d commit 442c205

6 files changed

Lines changed: 276 additions & 1 deletion

File tree

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ testpaths =
55
python_files = *_test.py *_tests.py
66
addopts =
77
-rf --import-mode=importlib --strict-markers
8+
--ignore=tests/dcn_bandwidth_test.py
89
--ignore=tests/post_training/integration/grpo_trainer_correctness_test.py
910
--ignore=tests/integration/smoke/train_gpu_smoke_test.py
1011
--ignore=tests/integration/smoke/train_int8_smoke_test.py

src/maxtext/configs/base.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,15 @@ enable_diloco: false
884884
diloco_sync_period: 36
885885
diloco_outer_lr: 0.3
886886
diloco_outer_momentum: 0.9
887+
# DCN bandwidth throttling parameters (used for simulating slow networks).
888+
# If dcn_bandwidth_limit is empty, no throttling is applied.
889+
dcn_bandwidth_limit: ""
890+
# The burst size parameter for the traffic control (tc) token bucket filter.
891+
dcn_bandwidth_burst: "10mb"
892+
# The latency parameter for the traffic control (tc) token bucket filter.
893+
dcn_bandwidth_latency: "50ms"
894+
# The network interface to apply throttling rules to.
895+
dcn_bandwidth_interface: "eth0"
887896

888897
# You may disable clipping by setting gradient_clipping_threshold to zero.
889898
gradient_clipping_threshold: 1.0

src/maxtext/configs/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,14 @@ class DilocoParams(BaseModel):
14821482
diloco_sync_period: int = Field(36, description="Diloco sync period.")
14831483
diloco_outer_lr: float = Field(0.3, description="learning rate for outer optimizer.")
14841484
diloco_outer_momentum: float = Field(0.9, description="momentum for outer optimizer.")
1485+
dcn_bandwidth_limit: str = Field(
1486+
"", description="Programmatic DCN egress bandwidth limit (e.g., '28gbit'). Empty means no limit."
1487+
)
1488+
dcn_bandwidth_burst: str = Field("10mb", description="Burst size for Token Bucket Filter (TBF) traffic shaping.")
1489+
dcn_bandwidth_latency: str = Field(
1490+
"50ms", description="Latency threshold for Token Bucket Filter (TBF) traffic shaping."
1491+
)
1492+
dcn_bandwidth_interface: str = Field("eth0", description="Network interface to apply bandwidth limits on.")
14851493

14861494

14871495
class Optimizer(BaseModel):

src/maxtext/trainers/pre_train/train.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,10 @@ def train_loop(config, recorder, state=None):
619619
state,
620620
) = train_utils.setup_train_loop(config, recorder)
621621

622+
# Throttling is applied only if configured (dcn_bandwidth_limit is set).
623+
# The default flag value is empty, meaning no throttling is applied by default.
624+
train_utils.maybe_apply_dcn_throttling(config)
625+
622626
start_step = get_first_step(model, state) # this is the start_step for training
623627
train_utils.validate_completed_steps(start_step, config.steps)
624628

@@ -751,6 +755,7 @@ def train_loop(config, recorder, state=None):
751755
if _job_completed_gracefully:
752756
record_goodput(recorder, RECORD_JOB_END_TIME)
753757
metric_logger_instance.flush_metrics_and_cleanup()
758+
train_utils.maybe_cleanup_dcn_throttling(config)
754759

755760
return state
756761

src/maxtext/utils/train_utils.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
# pylint: disable=bare-except, consider-using-generator
1616
"""Utils that are only interesting for training in MaxText."""
1717

18+
import subprocess
19+
import jax
1820
import functools
1921
from functools import partial
2022

2123
from flax import nnx
2224
from flax.linen import partitioning as nn_partitioning
23-
import jax
25+
2426
from maxtext.common import checkpointing
2527
from maxtext.common import train_state_nnx
2628
from maxtext.common.common_types import ReorderStrategy
@@ -378,3 +380,53 @@ def validate_completed_steps(completed_steps: int, config_steps: int):
378380
f"Did you mean to continue training past step {completed_steps} (you should set steps > {completed_steps}) "
379381
f"or to not load the checkpoint (use enable_checkpointing=False?)"
380382
)
383+
384+
385+
def maybe_apply_dcn_throttling(config):
386+
"""Applies programmatic traffic control (tc) bandwidth limit if configured."""
387+
if not config.dcn_bandwidth_limit:
388+
return
389+
390+
interface = config.dcn_bandwidth_interface
391+
392+
# Always clean up any existing traffic control rule on the interface first.
393+
try:
394+
subprocess.run(
395+
["tc", "qdisc", "del", "dev", interface, "root"],
396+
stdout=subprocess.DEVNULL,
397+
stderr=subprocess.DEVNULL,
398+
check=False,
399+
)
400+
except Exception as e: # pylint: disable=broad-exception-caught
401+
max_logging.error(f"Failed to clean up existing traffic control on {interface}: {e}")
402+
403+
rate = config.dcn_bandwidth_limit
404+
burst = config.dcn_bandwidth_burst
405+
latency = config.dcn_bandwidth_latency
406+
407+
max_logging.log(f"Applying tc egress limit of {rate} (burst: {burst}, latency: {latency}) on {interface}...")
408+
try:
409+
cmd = ["tc", "qdisc", "add", "dev", interface, "root", "tbf", "rate", rate, "burst", burst, "latency", latency]
410+
subprocess.run(cmd, check=True)
411+
max_logging.log("DCN Bandwidth throttling applied successfully.")
412+
except Exception as e: # pylint: disable=broad-exception-caught
413+
max_logging.error(f"Failed to apply DCN bandwidth throttling: {e}")
414+
415+
416+
def maybe_cleanup_dcn_throttling(config):
417+
"""Cleans up traffic control (tc) rules."""
418+
if not config.dcn_bandwidth_limit:
419+
return
420+
421+
interface = config.dcn_bandwidth_interface
422+
max_logging.log(f"Cleaning up tc egress limit on {interface}...")
423+
try:
424+
subprocess.run(
425+
["tc", "qdisc", "del", "dev", interface, "root"],
426+
stdout=subprocess.DEVNULL,
427+
stderr=subprocess.DEVNULL,
428+
check=False,
429+
)
430+
max_logging.log("DCN Bandwidth throttling cleaned up successfully.")
431+
except Exception as e: # pylint: disable=broad-exception-caught
432+
max_logging.error(f"Failed to clean up DCN bandwidth throttling: {e}")

tests/dcn_bandwidth_test.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# NOTE: This test/benchmark requires a multislice TPU setup to run correctly.
16+
# It is excluded from standard pytest discovery.
17+
18+
"""A microbenchmark to test DCN network bandwidth using shard map.
19+
20+
This script should be run on a multi-slice TPU cluster (specifically across 2 slices
21+
with any ICI/slice dimensions
22+
"""
23+
24+
import datetime
25+
import functools
26+
import subprocess
27+
from types import SimpleNamespace
28+
29+
import jax
30+
from jax.experimental import mesh_utils
31+
from jax.experimental.shard_map import shard_map
32+
import jax.numpy as jnp
33+
from jax.sharding import Mesh
34+
from jax.sharding import PartitionSpec as P
35+
36+
from maxtext.utils import train_utils
37+
38+
39+
def get_default_interface():
40+
try:
41+
route_output = subprocess.check_output("ip route show", shell=True, text=True)
42+
for line in route_output.splitlines():
43+
if "default" in line:
44+
return line.split("dev")[1].strip().split()[0]
45+
except (subprocess.SubprocessError, IndexError):
46+
pass
47+
return "eth0"
48+
49+
50+
def simple_timeit(f, *args, tries=10, task=None):
51+
"""Simple utility to time a function for multiple runs."""
52+
assert task is not None
53+
outcomes_ms = []
54+
55+
# Warm up
56+
jax.block_until_ready(f(*args))
57+
58+
for _ in range(tries):
59+
jax.devices() # Force synchronization
60+
s = datetime.datetime.now()
61+
jax.block_until_ready(f(*args))
62+
e = datetime.datetime.now()
63+
outcomes_ms.append(1000 * (e - s).total_seconds())
64+
65+
average_time_ms = sum(outcomes_ms) / len(outcomes_ms)
66+
return average_time_ms
67+
68+
69+
def create_mesh(dcn_size: int, ici_size: int):
70+
"""Creates a hybrid mesh with DCN and ICI axes."""
71+
dcn_parallelism = [dcn_size, 1]
72+
ici_parallelism = [1, ici_size]
73+
74+
total_devices = jax.device_count()
75+
if total_devices != (dcn_size * ici_size):
76+
raise ValueError(f"Need {dcn_size * ici_size} devices, but found {total_devices}")
77+
mesh_devices = mesh_utils.create_hybrid_device_mesh(ici_parallelism, dcn_parallelism, devices=jax.devices())
78+
mesh = Mesh(mesh_devices, ("dcn", "ici"))
79+
return mesh
80+
81+
82+
def run_dcn_benchmark(case=None, limit=None, burst=None, latency="50ms"):
83+
"""Runs the DCN bandwidth benchmark for specified throttling cases."""
84+
print(f"JAX process index: {jax.process_index()} / {jax.process_count()}")
85+
print(f"Total devices: {jax.device_count()}, local devices: {jax.local_device_count()}")
86+
87+
dcn_size = 2
88+
total_devices = jax.device_count()
89+
if total_devices % dcn_size != 0:
90+
raise ValueError(f"Total devices ({total_devices}) must be divisible by dcn_size ({dcn_size}) for 2-slice setup.")
91+
ici_size = total_devices // dcn_size
92+
mesh = create_mesh(dcn_size, ici_size)
93+
94+
# Predefined cases
95+
all_cases = [
96+
("none", "NO THROTTLING (Baseline)", False, None, None),
97+
("100g", "100G Throttling", True, "100gbit", "600mb"),
98+
("50g", "50G Throttling", True, "50gbit", "300mb"),
99+
]
100+
101+
# Filter based on arguments if requested
102+
if case:
103+
cases_to_run = [c for c in all_cases if c[0] == case]
104+
if not cases_to_run:
105+
# If custom limit/burst are provided
106+
if limit and burst:
107+
cases_to_run = [(case, f"CUSTOM Throttling ({case})", True, limit, burst)]
108+
else:
109+
raise ValueError(f"Unknown case: {case}")
110+
else:
111+
cases_to_run = all_cases
112+
113+
# Qwen3-30B MoE layer weight shape: (128, 2048, 768)
114+
shape = (128, 2048, 768)
115+
dtype = jnp.bfloat16
116+
117+
# Calculate size
118+
num_elements = 1
119+
for d in shape:
120+
num_elements *= d
121+
matrix_size_gbyte = num_elements * dtype.dtype.itemsize / 1e9
122+
123+
# We define shard map collective psum along the DCN axis.
124+
# Input x is sharded across 'dcn' axis.
125+
@functools.partial(shard_map, mesh=mesh, in_specs=P("dcn", None, None), out_specs=P(None, None, None))
126+
def psum_dcn_op(x):
127+
return jax.lax.psum(x, "dcn")
128+
129+
# Initialize matrix
130+
matrix = jnp.ones(shape, dtype=dtype)
131+
132+
# Pre-distribute the matrix shard onto devices
133+
sharded_matrix = jax.device_put(matrix, jax.sharding.NamedSharding(mesh, P("dcn", None, None)))
134+
135+
jitted_op = jax.jit(psum_dcn_op)
136+
137+
interface = get_default_interface()
138+
139+
for _, name, apply_throttling, dcn_limit, dcn_burst in cases_to_run:
140+
if jax.process_index() == 0:
141+
print("\n==================================================")
142+
print(f"Running Case: {name}")
143+
if apply_throttling:
144+
print(f" Throttling Config: limit={dcn_limit}, burst={dcn_burst}, latency={latency}")
145+
print("==================================================")
146+
147+
if apply_throttling:
148+
config = SimpleNamespace(
149+
dcn_bandwidth_limit=dcn_limit,
150+
dcn_bandwidth_burst=dcn_burst,
151+
dcn_bandwidth_latency=latency,
152+
dcn_bandwidth_interface=interface,
153+
)
154+
train_utils.maybe_apply_dcn_throttling(config)
155+
else:
156+
config = None
157+
158+
try:
159+
# Sync before starting benchmark
160+
jax.block_until_ready(jax.device_put(0.0) + 1.0)
161+
162+
if jax.process_index() == 0:
163+
print(f"Starting benchmark for shape: {shape} ({matrix_size_gbyte * 1000:.1f} MB)")
164+
165+
# Run time test
166+
time_ms = simple_timeit(jitted_op, sharded_matrix, task=f"psum_dcn_{shape}")
167+
168+
# Calculate Bandwidth
169+
achieved_bandwidth_gbyte_s = matrix_size_gbyte * (dcn_size - 1) * 2 / dcn_size / dcn_size / (time_ms / 1e3)
170+
achieved_bandwidth_gbps = achieved_bandwidth_gbyte_s * 8.0
171+
172+
if jax.process_index() == 0:
173+
print(f"Results for {name}:")
174+
print(f" Avg Latency: {time_ms:.2f} ms")
175+
print(
176+
f" Achieved DCN Bandwidth: {achieved_bandwidth_gbyte_s:.3f} GB/s ({achieved_bandwidth_gbps:.2f} Gbps) per slice"
177+
)
178+
finally:
179+
if apply_throttling and config:
180+
if jax.process_index() == 0:
181+
print(f"Cleaning up throttling for {name}...")
182+
train_utils.maybe_cleanup_dcn_throttling(config)
183+
184+
# Sync after cleanup
185+
jax.block_until_ready(jax.device_put(0.0) + 1.0)
186+
187+
188+
if __name__ == "__main__":
189+
import argparse
190+
191+
parser = argparse.ArgumentParser(description="DCN Bandwidth Benchmark")
192+
parser.add_argument(
193+
"--case", choices=["none", "100g", "50g"], help="Predefined throttling case (optional, runs all if omitted)"
194+
)
195+
parser.add_argument("--limit", help="Custom DCN bandwidth limit (e.g. 100gbit)")
196+
parser.add_argument("--burst", help="Custom DCN bandwidth burst (e.g. 600mb)")
197+
parser.add_argument("--latency", default="50ms", help="DCN bandwidth latency (default: 50ms)")
198+
parsed_args = parser.parse_args()
199+
200+
run_dcn_benchmark(case=parsed_args.case, limit=parsed_args.limit, burst=parsed_args.burst, latency=parsed_args.latency)

0 commit comments

Comments
 (0)