Shardflow / tests /test_e2e_localhost.py
rautaditya2606's picture
first commit
18b1397
Raw
History Blame Contribute Delete
5.89 kB
"""
End-to-end test — split a model across 2 processes on localhost and generate text.
This script:
1. Splits the model into 2 node processes (layers 0-11 and layers 11-22)
2. Starts both nodes as async tasks on different ports
3. Starts the orchestrator
4. Generates text from a prompt
5. Verifies the output is coherent
Usage:
python -m tests.test_e2e_localhost --model ./models/TinyLlama-1.1B-Chat-v1.0
"""
import argparse
import asyncio
import logging
import sys
import time
import os
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import torch
from transformers import AutoConfig
from shardflow.node.layer_loader import load_layer_slice
from shardflow.node.node import PipelineNode
from shardflow.orchestrator.orchestrator import Orchestrator
logger = logging.getLogger(__name__)
async def run_e2e_test(
model_path: str,
prompt: str = "Once upon a time",
max_tokens: int = 30,
temperature: float = 0.0,
device: str = "cpu",
num_nodes: int = 2,
):
"""
Run a full end-to-end test with the model split across N nodes on localhost.
"""
config = AutoConfig.from_pretrained(model_path)
total_layers = config.num_hidden_layers
logger.info("Model: %s, layers: %d, hidden_size: %d", model_path, total_layers, config.hidden_size)
# Calculate layer ranges for each node
layers_per_node = total_layers // num_nodes
remainder = total_layers % num_nodes
layer_ranges = []
start = 0
for i in range(num_nodes):
end = start + layers_per_node + (1 if i < remainder else 0)
layer_ranges.append((start, end))
start = end
logger.info("Layer partition: %s", layer_ranges)
# Base port for nodes
base_port = 9000
# --- Load model slices ---
logger.info("Loading model slices...")
slices = []
for i, (layer_start, layer_end) in enumerate(layer_ranges):
is_last = (i == num_nodes - 1)
logger.info("Loading node %d: layers [%d, %d), is_last=%s", i, layer_start, layer_end, is_last)
model_slice = load_layer_slice(
model_path=model_path,
layer_start=layer_start,
layer_end=layer_end,
include_norm=is_last,
include_lm_head=is_last,
device=device,
)
slices.append(model_slice)
# --- Create and start nodes ---
nodes = []
for i, (model_slice, (layer_start, layer_end)) in enumerate(zip(slices, layer_ranges)):
is_first = (i == 0)
is_last = (i == num_nodes - 1)
# Next node connection (not for the last node)
next_host = "127.0.0.1" if not is_last else None
next_port = base_port + i + 1 if not is_last else None
node = PipelineNode(
model_slice=model_slice,
is_first_node=is_first,
is_last_node=is_last,
next_node_host=next_host,
next_node_port=next_port,
listen_host="127.0.0.1",
listen_port=base_port + i,
)
nodes.append(node)
# Start nodes in REVERSE order (last node first, so connections succeed)
for i in reversed(range(len(nodes))):
logger.info("Starting node %d on port %d...", i, base_port + i)
await nodes[i].start()
# Brief delay to let the server socket bind
await asyncio.sleep(0.3)
logger.info("All %d nodes started", num_nodes)
# --- Create and run orchestrator ---
node_addresses = [(f"127.0.0.1", base_port + i) for i in range(num_nodes)]
orchestrator = Orchestrator(
model_path=model_path,
node_addresses=node_addresses,
device="cpu", # Embedding always on CPU
)
try:
await orchestrator.initialize()
print(f"\n{'='*60}")
print(f"Prompt: {prompt}")
print(f"Generating {max_tokens} tokens across {num_nodes} nodes...")
print(f"{'='*60}\n")
start_time = time.perf_counter()
result = await orchestrator.generate(
prompt=prompt,
max_tokens=max_tokens,
temperature=temperature,
stream=True,
)
total_time = time.perf_counter() - start_time
print(f"\n{'='*60}")
print(f"Completion: {result}")
print(f"Time: {total_time:.2f}s")
print(f"Tokens: {len(orchestrator.tokenizer.encode(result))}")
print(f"Speed: {len(orchestrator.tokenizer.encode(result)) / total_time:.1f} tok/s")
print(f"{'='*60}")
finally:
await orchestrator.shutdown()
for node in nodes:
await node.stop()
def main():
parser = argparse.ArgumentParser(description="ShardFlow E2E Test")
parser.add_argument(
"--model",
default="./models/TinyLlama-1.1B-Chat-v1.0",
help="Model path",
)
parser.add_argument("--prompt", default="Once upon a time", help="Input prompt")
parser.add_argument("--max-tokens", type=int, default=30, help="Max tokens to generate")
parser.add_argument("--temperature", type=float, default=0.0, help="Sampling temperature")
parser.add_argument("--num-nodes", type=int, default=2, help="Number of nodes to split across")
parser.add_argument(
"--device",
default="cuda" if torch.cuda.is_available() else "cpu",
help="Device for nodes (cpu or cuda)",
)
parser.add_argument("--log-level", default="INFO")
args = parser.parse_args()
logging.basicConfig(
level=getattr(logging, args.log_level),
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
asyncio.run(run_e2e_test(
model_path=args.model,
prompt=args.prompt,
max_tokens=args.max_tokens,
temperature=args.temperature,
device=args.device,
num_nodes=args.num_nodes,
))
if __name__ == "__main__":
main()