Spaces:
Paused
Paused
File size: 5,888 Bytes
18b1397 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | """
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()
|