Files
2026-07-13 13:18:33 +08:00

147 lines
5.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""Conformance tests for the RolloutEngine interface.
Validates the dataclass invariants and exercises the interface against a
``FakeRollout`` so the contract is testable without GPUs or a model. The real
backends are tested manually with a launched training script (see README).
"""
import pytest
import torch
from deepspeed.runtime.rollout import (
RolloutBatch,
RolloutEngine,
RolloutRequest,
SamplingConfig,
build_rollout,
)
# --- dataclass invariants ---------------------------------------------------
def test_rollout_request_validates_shapes():
with pytest.raises(ValueError, match="must be 2-D"):
RolloutRequest(prompt_ids=torch.zeros(8), prompt_attention_mask=torch.ones(8))
with pytest.raises(ValueError, match="does not match"):
RolloutRequest(prompt_ids=torch.zeros(2, 4, dtype=torch.long), prompt_attention_mask=torch.ones(2, 5))
def test_rollout_batch_validates_shapes():
with pytest.raises(ValueError, match="must be 2-D"):
RolloutBatch(input_ids=torch.zeros(8, dtype=torch.long),
attention_mask=torch.ones(8),
response_start_idx=torch.tensor([4]))
with pytest.raises(ValueError, match="does not match"):
RolloutBatch(input_ids=torch.zeros(2, 4, dtype=torch.long),
attention_mask=torch.ones(2, 5),
response_start_idx=torch.tensor([4, 4]))
with pytest.raises(ValueError, match="1-D of length"):
RolloutBatch(input_ids=torch.zeros(2, 4, dtype=torch.long),
attention_mask=torch.ones(2, 4),
response_start_idx=torch.tensor([4]))
def test_rollout_batch_accessors():
batch = RolloutBatch(
input_ids=torch.zeros(3, 12, dtype=torch.long),
attention_mask=torch.ones(3, 12),
response_start_idx=torch.tensor([4, 5, 6]),
)
assert batch.batch_size == 3
assert batch.seq_len == 12
def test_sampling_config_defaults():
cfg = SamplingConfig(max_new_tokens=32)
assert cfg.temperature == 1.0
assert cfg.top_p == 1.0
assert cfg.top_k == -1
assert cfg.n_samples_per_prompt == 1
# --- interface conformance via FakeRollout ---------------------------------
class FakeRollout(RolloutEngine):
"""Deterministic stub: appends ``[42] * max_new_tokens`` to each prompt."""
name = "fake"
def __init__(self, response_token: int = 42):
self.response_token = response_token
self.sync_calls: list = []
def generate(self, request: RolloutRequest, sampling: SamplingConfig) -> RolloutBatch:
B, T_p = request.prompt_ids.shape
n = sampling.n_samples_per_prompt
T_r = sampling.max_new_tokens
prompts_expanded = request.prompt_ids.repeat_interleave(n, dim=0)
attn_p_expanded = request.prompt_attention_mask.repeat_interleave(n, dim=0)
response = torch.full((B * n, T_r), self.response_token, dtype=request.prompt_ids.dtype)
response_attn = torch.ones((B * n, T_r), dtype=attn_p_expanded.dtype)
input_ids = torch.cat([prompts_expanded, response], dim=1)
attention_mask = torch.cat([attn_p_expanded, response_attn], dim=1)
response_start_idx = torch.full((B * n, ), T_p, dtype=torch.long)
return RolloutBatch(input_ids=input_ids, attention_mask=attention_mask, response_start_idx=response_start_idx)
def sync_weights(self, step: int) -> None:
self.sync_calls.append(step)
def test_fake_rollout_shape_basic():
fake = FakeRollout()
req = RolloutRequest(prompt_ids=torch.tensor([[1, 2, 3], [4, 5, 6]]),
prompt_attention_mask=torch.ones(2, 3, dtype=torch.long))
out = fake.generate(req, SamplingConfig(max_new_tokens=4))
assert out.input_ids.shape == (2, 7)
assert out.attention_mask.shape == (2, 7)
# With left-padded (fully real here) prompts of width 3, response begins
# at column 3 for every sample.
assert out.response_start_idx.tolist() == [3, 3]
def test_fake_rollout_with_n_samples():
fake = FakeRollout()
req = RolloutRequest(prompt_ids=torch.tensor([[1, 2], [3, 4]]),
prompt_attention_mask=torch.ones(2, 2, dtype=torch.long))
out = fake.generate(req, SamplingConfig(max_new_tokens=3, n_samples_per_prompt=4))
assert out.input_ids.shape == (8, 5)
assert out.response_start_idx.tolist() == [2] * 8
def test_fake_rollout_left_padded_prompts():
fake = FakeRollout()
# left-padded prompts: prompt B has only the last 2 positions real, but
# response_start_idx still equals the prompt column width T_p.
prompt_ids = torch.tensor([[1, 2, 3, 4], [0, 0, 5, 6]])
attn = torch.tensor([[1, 1, 1, 1], [0, 0, 1, 1]], dtype=torch.long)
req = RolloutRequest(prompt_ids=prompt_ids, prompt_attention_mask=attn)
out = fake.generate(req, SamplingConfig(max_new_tokens=2))
assert out.response_start_idx.tolist() == [4, 4]
def test_sync_records_steps():
fake = FakeRollout()
fake.sync_weights(0)
fake.sync_weights(5)
assert fake.sync_calls == [0, 5]
def test_engine_factory_unknown_raises():
from deepspeed.runtime.rollout.base import RolloutConfig
with pytest.raises(ValueError, match="Unknown rollout engine"):
build_rollout(RolloutConfig(engine="totally_made_up"))
def test_engine_factory_hybrid_requires_student_engine():
from deepspeed.runtime.rollout.base import RolloutConfig
with pytest.raises(ValueError, match="needs both"):
build_rollout(RolloutConfig(engine="hybrid_engine"))