Files
ray-project--ray/python/ray/train/v2/tests/test_jax_trainer.py
T
2026-07-13 13:17:40 +08:00

992 lines
33 KiB
Python

import os
import sys
import pytest
import ray
from ray.tests.conftest import _ray_start_cluster
from ray.train import RunConfig, ScalingConfig, UserCallback
from ray.train.v2._internal.constants import (
HEALTH_CHECK_INTERVAL_S_ENV_VAR,
is_v2_enabled,
)
from ray.train.v2.jax import JaxTrainer
assert is_v2_enabled()
@pytest.fixture
def ray_tpu_single_host():
"""Start a mock single-host TPU Ray cluster with 2x4 v6e (8 chips per host)."""
with _ray_start_cluster() as cluster:
# Simulate one node with 8 TPU chips.
cluster.add_node(
num_cpus=4,
resources={"TPU": 8, "accelerator_type:TPU-V6E": 1},
env_vars={"TPU_ACCELERATOR_TYPE": "v6e-8"},
)
ray.init(address=cluster.address)
yield cluster
ray.shutdown()
@pytest.fixture
def ray_tpu_multi_host():
"""
Simulates a Ray cluster with two multi-host TPU v4-16 slices.
"""
pod_type = "v4-16"
topology = "2x2x2"
with _ray_start_cluster() as cluster:
# First TPU slice - v4-16 with 2 hosts and 4 chips/host.
slice_a_head_env = {
"TPU_NAME": "slice-A",
"TPU_WORKER_ID": "0",
"TPU_ACCELERATOR_TYPE": pod_type,
"TPU_TOPOLOGY": topology,
}
slice_a_head_labels = {
"ray.io/tpu-slice-name": "slice-A",
"ray.io/tpu-worker-id": "0",
"ray.io/tpu-pod-type": pod_type,
}
slice_a_worker_env = {
"TPU_NAME": "slice-A",
"TPU_WORKER_ID": "1",
"TPU_ACCELERATOR_TYPE": pod_type,
"TPU_TOPOLOGY": topology,
}
slice_a_worker_labels = {
"ray.io/tpu-slice-name": "slice-A",
"ray.io/tpu-worker-id": "1",
"ray.io/tpu-pod-type": pod_type,
}
cluster.add_node(
num_cpus=8,
resources={
"TPU": 4,
f"TPU-{pod_type}-head": 1,
"accelerator_type:TPU-V4": 1,
},
env_vars=slice_a_head_env,
labels=slice_a_head_labels,
)
cluster.add_node(
num_cpus=8,
resources={"TPU": 4, "accelerator_type:TPU-V4": 1},
env_vars=slice_a_worker_env,
labels=slice_a_worker_labels,
)
# Second TPU slice - v4-16 with 2 hosts and 4 chips/host.
slice_b_head_env = {
"TPU_NAME": "slice-B",
"TPU_WORKER_ID": "0",
"TPU_ACCELERATOR_TYPE": pod_type,
"TPU_TOPOLOGY": topology,
}
slice_b_head_labels = {
"ray.io/tpu-slice-name": "slice-B",
"ray.io/tpu-worker-id": "0",
"ray.io/tpu-pod-type": pod_type,
}
slice_b_worker_env = {
"TPU_NAME": "slice-B",
"TPU_WORKER_ID": "1",
"TPU_ACCELERATOR_TYPE": pod_type,
"TPU_TOPOLOGY": topology,
}
slice_b_worker_labels = {
"ray.io/tpu-slice-name": "slice-B",
"ray.io/tpu-worker-id": "1",
"ray.io/tpu-pod-type": pod_type,
}
cluster.add_node(
num_cpus=8,
resources={
"TPU": 4,
f"TPU-{pod_type}-head": 1,
"accelerator_type:TPU-V4": 1,
},
env_vars=slice_b_head_env,
labels=slice_b_head_labels,
)
cluster.add_node(
num_cpus=8,
resources={"TPU": 4, "accelerator_type:TPU-V4": 1},
env_vars=slice_b_worker_env,
labels=slice_b_worker_labels,
)
ray.init(address=cluster.address)
yield cluster
ray.shutdown()
@pytest.fixture(autouse=True)
def reduce_health_check_interval(monkeypatch):
monkeypatch.setenv(HEALTH_CHECK_INTERVAL_S_ENV_VAR, "0.2")
yield
def train_func():
import jax
import ray
from ray import train
train_ctx = train.get_context()
rank = train_ctx.get_world_rank()
devices = jax.devices()
node_labels = ray.get_runtime_context().get_node_labels()
slice_name = node_labels.get("ray.io/tpu-slice-name")
current_ip = ray.util.get_node_ip_address()
megascale_vars = {
"MEGASCALE_SLICE_ID": os.environ.get("MEGASCALE_SLICE_ID"),
"MEGASCALE_NUM_SLICES": os.environ.get("MEGASCALE_NUM_SLICES"),
"MEGASCALE_COORDINATOR_ADDRESS": os.environ.get(
"MEGASCALE_COORDINATOR_ADDRESS"
),
"MEGASCALE_PORT": os.environ.get("MEGASCALE_PORT"),
}
train.report(
{
"worker_id": rank,
"slice_name": slice_name,
"node_ip": current_ip,
"devices": [str(d) for d in devices],
**megascale_vars,
}
)
class CustomMetricsCallback(UserCallback):
"""
In Ray Train V2, reporting metrics from all workers is a no-op, so we
utilize this callback to access the results in our tests.
"""
def __init__(self, actor_name: str):
self.actor_name = actor_name
def after_report(self, run_context, metrics, checkpoint):
# Connect to the specific verify actor for this test.
sink = ray.get_actor(self.actor_name)
sink.log.remote(metrics)
@ray.remote
class VerificationActor:
"""
This Actor is called from the custom metrics callback and saves
the reported metrics from each test.
"""
def __init__(self):
self.reports = []
def log(self, metrics):
self.reports.extend(metrics)
def get_reports(self):
return self.reports
def test_tpu_single_host(ray_tpu_single_host, tmp_path):
"""
Tests single-host scheduling with no topology value. In this case, the
JaxTrainer should skip the multi-host slice scheduling logic and setup
with a single TPU worker.
"""
actor_name = "test_tpu_single_host"
verify_actor = VerificationActor.options(name=actor_name).remote()
trainer = JaxTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(
use_tpu=True,
num_workers=1,
resources_per_worker={"TPU": 8},
accelerator_type="TPU-V6E",
),
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={"env_vars": {"JAX_PLATFORMS": "cpu"}},
),
)
result = trainer.fit()
assert result.error is None
# Fetch metrics result using the verification actor.
reports = ray.get(verify_actor.get_reports.remote())
# The train func should have ran on one single-host TPU.
assert len(reports) == 1, f"Expected 1 report, got {len(reports)}"
report = reports[0]
assert report["worker_id"] == 0
# Validate we do not automatically set megascale vars for single-slice.
for r in reports:
assert r.get("MEGASCALE_SLICE_ID") is None
assert r.get("MEGASCALE_NUM_SLICES") is None
assert r.get("MEGASCALE_COORDINATOR_ADDRESS") is None
def test_tpu_single_slice_multi_host(ray_tpu_multi_host, tmp_path):
"""
Tests scheduling on a single multi-host slice. The number of workers
is set by the user to match the number of hosts in the slice, with each
worker consuming the full resources on that host.
"""
actor_name = "test_tpu_single_slice_multi_host"
verify_actor = VerificationActor.options(name=actor_name).remote()
trainer = JaxTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(
use_tpu=True,
accelerator_type="TPU-V4",
topology="2x2x2",
num_workers=2,
),
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={"env_vars": {"JAX_PLATFORMS": "cpu"}},
),
)
result = trainer.fit()
assert result.error is None
# Fetch metrics result from each worker using the verification actor.
reports = ray.get(verify_actor.get_reports.remote())
# Verify two TPU workers on the same slice ran the training func.
assert (
len(reports) == 2
), f"Expected 2 workers to report metrics, got {len(reports)}"
worker_ids = {r["worker_id"] for r in reports}
assert worker_ids == {0, 1}, "Expected unique worker IDs from 0 to N-1."
slices_used = {r["slice_name"] for r in reports}
assert len(slices_used) == 1, "Expected workers to be scheduled to 1 slice."
assert next(iter(slices_used)) in ("slice-A", "slice-B")
# Validate we do not automatically set megascale vars for single-slice.
for r in reports:
assert r.get("MEGASCALE_SLICE_ID") is None
assert r.get("MEGASCALE_NUM_SLICES") is None
assert r.get("MEGASCALE_COORDINATOR_ADDRESS") is None
def test_tpu_multi_slice_multi_host(ray_tpu_multi_host, tmp_path):
"""
Tests execution of TPU workers across multiple multi-host slices. The
user specifies num_workers equal to the total number of hosts across all
slices.
"""
actor_name = "test_tpu_multi_slice_multi_host"
verify_actor = VerificationActor.options(name=actor_name).remote()
trainer = JaxTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(
use_tpu=True,
accelerator_type="TPU-V4",
topology="2x2x2",
num_workers=4,
),
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={"env_vars": {"JAX_PLATFORMS": "cpu"}},
),
)
result = trainer.fit()
assert result.error is None
# Fetch metrics result from each worker using the verification actor.
reports = ray.get(verify_actor.get_reports.remote())
# Verify execution of all 4 TPU workers across both slices.
assert (
len(reports) == 4
), f"Expected 4 workers to report metrics, got {len(reports)}"
worker_ids = {r["worker_id"] for r in reports}
assert worker_ids == {0, 1, 2, 3}, "Expected unique worker IDs from 0 to N-1."
slices_used = {r["slice_name"] for r in reports}
assert len(slices_used) == 2, "Expected workers to schedule across 2 slices."
assert "slice-A" in slices_used
assert "slice-B" in slices_used
# Verify megascale coordinator address set to IP of worker 0.
worker_0_report = next(r for r in reports if r["worker_id"] == 0)
expected_coordinator_ip = worker_0_report["node_ip"]
for r in reports:
assert r["MEGASCALE_COORDINATOR_ADDRESS"] == expected_coordinator_ip
assert r["MEGASCALE_NUM_SLICES"] == "2"
# When the user does not set MEGASCALE_PORT in their pod spec, Ray
# Train falls back to the default port from
# ``get_tpu_coordinator_env_vars``.
assert r["MEGASCALE_PORT"] == "8081"
# Validate MEGASCALE_SLICE_ID set based on indexed TPU Pod name.
slice_a_reports = [r for r in reports if r["slice_name"] == "slice-A"]
slice_b_reports = [r for r in reports if r["slice_name"] == "slice-B"]
assert list({r["MEGASCALE_SLICE_ID"] for r in slice_a_reports}) == ["0"]
assert list({r["MEGASCALE_SLICE_ID"] for r in slice_b_reports}) == ["1"]
def test_tpu_multi_slice_overrides_stale_megascale_env(ray_tpu_multi_host, tmp_path):
"""
Tests JaxTrainer's MEGASCALE_* env var handling when the underlying TPU
node provider has already baked values into the pod environment.
This simulates the multi-slice fault tolerance scenario where one slice was
preempted and the replacement pods were provisioned by the TPU node
provider with env vars reflecting a different slice id / total slice count
than the worker group Ray Train is currently scheduling. Without an
override, libtpu's megascale topology coordinator would wait for slices
that no longer exist and hang TPU initialization.
"""
actor_name = "test_tpu_multi_slice_overrides_stale_megascale_env"
verify_actor = VerificationActor.options(name=actor_name).remote()
user_port_override = "9999"
trainer = JaxTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(
use_tpu=True,
accelerator_type="TPU-V4",
topology="2x2x2",
num_workers=4,
),
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
# Stale values, as the TPU node provider would inject on
# a freshly-provisioned replacement slice. These should
# all be overridden by Ray Train.
"MEGASCALE_COORDINATOR_ADDRESS": "stale-coordinator:9999",
"MEGASCALE_NUM_SLICES": "3",
"MEGASCALE_SLICE_ID": "2",
# User-customized port (e.g. set in pod spec to avoid a
# conflict with another process). This should be
# preserved by Ray Train, NOT overridden.
"MEGASCALE_PORT": user_port_override,
}
},
),
)
result = trainer.fit()
assert result.error is None
reports = ray.get(verify_actor.get_reports.remote())
assert (
len(reports) == 4
), f"Expected 4 workers to report metrics, got {len(reports)}"
# All workers should see the controller-computed coordinator address (the
# IP of worker 0), NOT the stale "stale-coordinator:9999" value.
worker_0_report = next(r for r in reports if r["worker_id"] == 0)
expected_coordinator_ip = worker_0_report["node_ip"]
for r in reports:
assert r["MEGASCALE_COORDINATOR_ADDRESS"] == expected_coordinator_ip, (
"Expected MEGASCALE_COORDINATOR_ADDRESS to be overridden by Ray "
f"Train, but worker {r['worker_id']} still has stale value "
f"{r['MEGASCALE_COORDINATOR_ADDRESS']}"
)
assert r["MEGASCALE_NUM_SLICES"] == "2", (
"Expected MEGASCALE_NUM_SLICES to be overridden to the actual "
f"slice count (2), but worker {r['worker_id']} still has stale "
f"value {r['MEGASCALE_NUM_SLICES']}"
)
assert r["MEGASCALE_PORT"] == user_port_override, (
"Expected MEGASCALE_PORT to preserve the user-provided pod-spec "
f"value '{user_port_override}', but worker {r['worker_id']} has "
f"value '{r['MEGASCALE_PORT']}'. Ray Train should not override "
"user-customized ports."
)
# Validate MEGASCALE_SLICE_ID was overridden per slice (0 / 1) rather than
# the stale "2" the provider injected on every worker.
slice_a_reports = [r for r in reports if r["slice_name"] == "slice-A"]
slice_b_reports = [r for r in reports if r["slice_name"] == "slice-B"]
assert list({r["MEGASCALE_SLICE_ID"] for r in slice_a_reports}) == ["0"]
assert list({r["MEGASCALE_SLICE_ID"] for r in slice_b_reports}) == ["1"]
def test_multi_slice_manual_resources(ray_tpu_multi_host, tmp_path):
"""
Tests execution of TPU workers across multiple multi-host slices when
`resources_per_worker` is specified. The JaxTrainer should execute across
both slices with num_workers workers of the specified resources.
"""
actor_name = "test_multi_slice_manual_resources"
verify_actor = VerificationActor.options(name=actor_name).remote()
trainer = JaxTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(
use_tpu=True,
accelerator_type="TPU-V4",
topology="2x2x2",
resources_per_worker={"TPU": 1}, # 1 CPU added by default per-bundle.
num_workers=16,
),
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={"env_vars": {"JAX_PLATFORMS": "cpu"}},
),
)
result = trainer.fit()
assert result.error is None
# Fetch metrics result from each worker using the verification actor.
reports = ray.get(verify_actor.get_reports.remote())
# Verify execution of all 16 TPU workers across both v4-16 slices.
assert (
len(reports) == 16
), f"Expected 16 workers to report metrics, got {len(reports)}"
worker_ids = {r["worker_id"] for r in reports}
assert worker_ids == set(range(16)), "Expected unique worker IDs from 0 to N-1."
slices_used = {r["slice_name"] for r in reports}
assert len(slices_used) == 2, "Expected workers to span 2 slices."
assert "slice-A" in slices_used
assert "slice-B" in slices_used
# Verify megascale coordinator address set to IP of worker 0.
worker_0_report = next(r for r in reports if r["worker_id"] == 0)
expected_coordinator_ip = worker_0_report["node_ip"]
for r in reports:
assert r["MEGASCALE_COORDINATOR_ADDRESS"] == expected_coordinator_ip
assert r["MEGASCALE_NUM_SLICES"] == "2"
# Validate MEGASCALE_SLICE_ID set based on indexed TPU Pod name.
slice_a_reports = [r for r in reports if r["slice_name"] == "slice-A"]
slice_b_reports = [r for r in reports if r["slice_name"] == "slice-B"]
assert list({r["MEGASCALE_SLICE_ID"] for r in slice_a_reports}) == ["0"]
assert list({r["MEGASCALE_SLICE_ID"] for r in slice_b_reports}) == ["1"]
def test_tpu_multi_slice_uneven_workers(ray_tpu_multi_host, tmp_path):
"""
Tests that ScalingConfig raises a ValueError if the requested num_workers
does not divide evenly across TPU slices of the requested topology.
"""
# Default resources (1 worker per host).
with pytest.raises(ValueError, match="must be a multiple of"):
ScalingConfig(
use_tpu=True,
accelerator_type="TPU-V4",
topology="2x2x2",
num_workers=3, # Expect a multiple of 2.
)
# Explicit resources (1 TPU chip per worker).
with pytest.raises(ValueError, match="must be a multiple of"):
ScalingConfig(
use_tpu=True,
accelerator_type="TPU-V4",
topology="2x2x1",
resources_per_worker={"TPU": 1},
num_workers=6, # Expect a multiple of 4.
)
def train_func_with_data_multi_host():
import unittest.mock
import jax
import ray
from ray import train
train_ctx = train.get_context()
rank = train_ctx.get_world_rank()
devices = jax.devices()
local_devices = jax.local_devices()
node_labels = ray.get_runtime_context().get_node_labels()
slice_name = node_labels.get("ray.io/tpu-slice-name")
ds_shard = train.get_dataset_shard("train")
batches = []
# Mock process_allgather because the JAX CPU backend does not support multiprocess communication.
# Since our mock test distributes exact same data lengths uniformly, all workers report the identical
# array `[has_batch, local_batch_size]`. We simulate the gather by stacking the local array identically.
def mock_process_allgather(arr):
import jax.numpy as jnp
return jnp.stack([arr] * jax.process_count())
with unittest.mock.patch(
"jax.experimental.multihost_utils.process_allgather",
side_effect=mock_process_allgather,
):
# Each batch has 4 rows. iter_jax_batches should combine the batches from all workers and return a batch of size 4 * num_workers.
for batch in ds_shard.iter_jax_batches(batch_size=4, synchronize_batches=True):
batches.append(batch["id"].shape)
train.report(
{
"worker_id": rank,
"slice_name": slice_name,
"devices": len(devices),
"local_devices": len(local_devices),
"batches": batches,
}
)
def test_multi_host_data_iterator(ray_tpu_multi_host, tmp_path):
actor_name = "test_multi_host_data_iterator"
verify_actor = VerificationActor.options(name=actor_name).remote()
import ray
# Create 32 rows, batch size 4.
# We have 4 workers. Each custom shard will have 8 rows (2 batches).
ds = ray.data.range(32)
trainer = JaxTrainer(
train_loop_per_worker=train_func_with_data_multi_host,
scaling_config=ScalingConfig(
use_tpu=True,
accelerator_type="TPU-V4",
topology="2x2x2",
num_workers=4,
),
datasets={"train": ds},
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
"XLA_FLAGS": "--xla_force_host_platform_device_count=4",
}
},
),
)
result = trainer.fit()
assert result.error is None
# Fetch metrics result from each worker using the verification actor.
reports = ray.get(verify_actor.get_reports.remote())
# Verify execution of all 4 TPU workers across both slices.
assert (
len(reports) == 4
), f"Expected 4 workers to report metrics, got {len(reports)}"
worker_ids = {r["worker_id"] for r in reports}
assert worker_ids == {0, 1, 2, 3}, "Expected unique worker IDs from 0 to N-1."
for r in reports:
# With batch_size=4 and 8 rows per shard, each worker sees 2 batches.
# jax.devices() will return 16 global devices (CPU).
assert r["devices"] == 16
assert r["local_devices"] == 4
assert len(r["batches"]) == 2
# Each batch should have global batch size of 16.
for batch_shape in r["batches"]:
assert batch_shape == (16,)
def test_single_host_data_iterator_padding(ray_tpu_single_host, tmp_path):
actor_name = "test_single_host_data_iterator_padding"
verify_actor = VerificationActor.options(name=actor_name).remote()
import numpy as np
import ray
# Create 44 rows. Batch size 16.
# 44 / 16 = 2 batches of 16, and 1 batch of 12.
# With paddings, we expect 3 batches of 16.
ds = ray.data.from_items([{"features": np.ones((8,))} for _ in range(44)])
def train_func():
from ray import train
ds_shard = train.get_dataset_shard("train")
batches = []
for batch in ds_shard.iter_jax_batches(
batch_size=16,
paddings=-1,
):
batches.append(batch["features"].shape)
train.report({"batches": batches})
trainer = JaxTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(
use_tpu=True,
num_workers=1,
resources_per_worker={"TPU": 8},
accelerator_type="TPU-V6E",
),
datasets={"train": ds},
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
"XLA_FLAGS": "--xla_force_host_platform_device_count=8",
}
},
),
)
result = trainer.fit()
assert result.error is None
reports = ray.get(verify_actor.get_reports.remote())
assert len(reports) == 1
assert len(reports[0]["batches"]) == 3
for shape in reports[0]["batches"]:
assert shape == (16, 8)
def test_single_host_data_iterator_dtypes(ray_tpu_single_host, tmp_path):
actor_name = "test_single_host_data_iterator_dtypes"
verify_actor = VerificationActor.options(name=actor_name).remote()
import numpy as np
import ray
ds = ray.data.from_items([{"features": np.ones((8,))} for _ in range(32)])
def train_func():
import jax.numpy as jnp
from ray import train
ds_shard = train.get_dataset_shard("train")
batches = []
for batch in ds_shard.iter_jax_batches(
batch_size=16,
dtypes=jnp.float16,
):
assert batch["features"].dtype == jnp.float16
batches.append(batch["features"].shape)
train.report({"batches": batches})
trainer = JaxTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(
use_tpu=True,
num_workers=1,
resources_per_worker={"TPU": 8},
accelerator_type="TPU-V6E",
),
datasets={"train": ds},
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
"XLA_FLAGS": "--xla_force_host_platform_device_count=8",
}
},
),
)
result = trainer.fit()
assert result.error is None
reports = ray.get(verify_actor.get_reports.remote())
assert len(reports) == 1
assert len(reports[0]["batches"]) == 2
for shape in reports[0]["batches"]:
assert shape == (16, 8)
def train_func_with_data_single_host(config):
import jax
import numpy as np
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
from ray import train
devices = jax.devices()
local_devices = jax.local_devices()
# We requested 8 TPU resources per worker, so XLA_FLAGS should expose 8 mock local devices
assert len(devices) == 8
assert len(local_devices) == 8
# 8 local devices across a 2x4 mesh
mesh = Mesh(np.array(local_devices).reshape(2, 4), ("x", "y"))
named_sharding = NamedSharding(mesh, P("x", "y"))
ds_shard = train.get_dataset_shard("train")
batches = []
drop_last = config.get("drop_last", False) if config else False
# Local batch size must be evenly divisible by 8 (num_local_devices)
for batch in ds_shard.iter_jax_batches(
batch_size=16,
drop_last=drop_last,
):
arr = jax.device_put(batch["features"], named_sharding)
assert arr.sharding == named_sharding
batches.append(arr.shape)
train.report(
{
"devices": len(devices),
"local_devices": len(local_devices),
"batches": batches,
}
)
def test_single_host_data_iterator_2d(ray_tpu_single_host, tmp_path):
actor_name = "test_single_host_data_iterator_2d"
verify_actor = VerificationActor.options(name=actor_name).remote()
import numpy as np
import ray
# Create 48 rows. Each row is {"features": np.ones((8,))}
# We have 1 worker. The worker processes 48 rows in batches of 16.
# The first 3 batches are (16, 8) in shape.
ds = ray.data.from_items([{"features": np.ones((8,))} for _ in range(48)])
trainer = JaxTrainer(
train_loop_per_worker=train_func_with_data_single_host,
scaling_config=ScalingConfig(
use_tpu=True,
num_workers=1,
resources_per_worker={"TPU": 8},
accelerator_type="TPU-V6E",
),
datasets={"train": ds},
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
# Mock 8 local CPU devices exclusively for this worker process.
# This enables jax.device_put to successfully execute local 2D resharding
# completely in-memory because jax.process_count() defaults to 1.
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
"XLA_FLAGS": "--xla_force_host_platform_device_count=8",
}
},
),
)
result = trainer.fit()
assert result.error is None
reports = ray.get(verify_actor.get_reports.remote())
assert len(reports) == 1
for r in reports:
assert r["devices"] == 8
assert r["local_devices"] == 8
assert len(r["batches"]) == 3
for batch_shape in r["batches"]:
assert batch_shape == (16, 8)
def test_single_host_data_iterator_2d_truncation(ray_tpu_single_host, tmp_path):
actor_name = "test_single_host_data_iterator_2d_truncation"
verify_actor = VerificationActor.options(name=actor_name).remote()
import numpy as np
import ray
# Create 60 rows. Each row is {"features": np.ones((8,))}
# 60 is not divisible by 16, so the last 12 rows should be truncated.
# Thus, each local batch is (16, 8) in shape and we expect exactly 3 batches.
ds = ray.data.from_items([{"features": np.ones((8,))} for _ in range(60)])
trainer = JaxTrainer(
train_loop_per_worker=train_func_with_data_single_host,
train_loop_config={"drop_last": True},
scaling_config=ScalingConfig(
use_tpu=True,
num_workers=1,
resources_per_worker={"TPU": 8},
accelerator_type="TPU-V6E",
),
datasets={"train": ds},
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
# Mock 8 local CPU devices exclusively for this worker process.
# This enables jax.device_put to successfully execute local 2D resharding
# completely in-memory because jax.process_count() defaults to 1.
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
"XLA_FLAGS": "--xla_force_host_platform_device_count=8",
}
},
),
)
result = trainer.fit()
assert result.error is None
reports = ray.get(verify_actor.get_reports.remote())
assert len(reports) == 1
for r in reports:
assert r["devices"] == 8
assert r["local_devices"] == 8
assert len(r["batches"]) == 3
for batch_shape in r["batches"]:
assert batch_shape == (16, 8)
def test_single_host_data_iterator_2d_truncation_failure(ray_tpu_single_host, tmp_path):
import numpy as np
import ray
# Create 60 rows. Each row is {"features": np.ones((8,))}
# 60 is not divisible by 16. So if drop_last=False (the default), it should raise a ValueError.
ds = ray.data.from_items([{"features": np.ones((8,))} for _ in range(60)])
trainer = JaxTrainer(
train_loop_per_worker=train_func_with_data_single_host,
scaling_config=ScalingConfig(
use_tpu=True,
num_workers=1,
resources_per_worker={"TPU": 8},
accelerator_type="TPU-V6E",
),
datasets={"train": ds},
run_config=RunConfig(
storage_path=str(tmp_path),
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
"XLA_FLAGS": "--xla_force_host_platform_device_count=8",
}
},
),
)
with pytest.raises(
Exception, match="evenly divisible by the number of local JAX devices"
):
trainer.fit()
def test_scaling_config_validation():
with pytest.raises(
ValueError, match="Cannot set `label_selector` when `use_tpu=True`"
):
ScalingConfig(
num_workers=2,
use_tpu=True,
topology="2x2x2",
accelerator_type="TPU-V4",
label_selector={"subcluster": "my_subcluster"},
)
def train_func_with_collate_fn(config):
import numpy as np
from ray import train
from ray.data.collate_fn import NumpyBatchCollateFn
class CustomCollateFn(NumpyBatchCollateFn):
def __call__(self, batch):
# Combine "col1" and "col2" columns into a single "features" tensor
return np.stack((batch["col1"], batch["col2"]), axis=1)
ds_shard = train.get_dataset_shard("train")
batches = []
for batch in ds_shard.iter_jax_batches(
batch_size=16,
collate_fn=CustomCollateFn(),
):
# The output of collate_fn is now a single jax.Array (global sharding)
# Check a specific value: it should be [1.0, 2.0] at the first position
assert np.array_equal(batch[0], np.array([1.0, 2.0]))
batches.append(batch.shape)
train.report(
{
"batches": batches,
}
)
def test_single_host_data_iterator_collate_fn(ray_tpu_single_host, tmp_path):
actor_name = "test_single_host_data_iterator_collate_fn"
verify_actor = VerificationActor.options(name=actor_name).remote()
import ray
# Create 32 rows. Each row has "col1" (ones) and "col2" (twos).
ds = ray.data.from_items([{"col1": 1.0, "col2": 2.0} for _ in range(32)])
trainer = JaxTrainer(
train_loop_per_worker=train_func_with_collate_fn,
scaling_config=ScalingConfig(
use_tpu=True,
num_workers=1,
resources_per_worker={"TPU": 8},
accelerator_type="TPU-V6E",
),
datasets={"train": ds},
run_config=RunConfig(
storage_path=str(tmp_path),
callbacks=[CustomMetricsCallback(actor_name)],
worker_runtime_env={
"env_vars": {
"JAX_PLATFORMS": "cpu",
"XLA_FLAGS": "--xla_force_host_platform_device_count=8",
}
},
),
)
result = trainer.fit()
assert result.error is None
reports = ray.get(verify_actor.get_reports.remote())
assert len(reports) == 1
for r in reports:
# We expect 2 batches (32 rows / 16 batch_size)
# Each batch should have shape (16, 2)
assert len(r["batches"]) == 2
for batch_shape in r["batches"]:
assert batch_shape == (16, 2)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", "-x", __file__]))