fix: comprehensive review + 14 bug fixes + Phase 12/14 overhaul

Strict code review identified 30+ issues across correctness, performance,
and architecture. This commit addresses 14 of them with verified fixes,
restructures Phase 12 for honest continuous batching, and updates Phase 14
to target FA2 (RTX 5090 SM120 lacks TMEM required by FA4).

Bug fixes:
- FIX-01: Global cuBLAS handle (thread-local singleton, was per-call)
- FIX-02: Remove 19 unnecessary cudaDeviceSynchronize calls from kernels
- FIX-03: Qwen3 ChatML template (was plain text concatenation)
- FIX-04: EOS token from tokenizer (was hardcoded 151645)
- FIX-05: Storage tracks actual GPU device ordinal (was always Cuda(0))
- FIX-06: unsqueeze stride preserves contiguous layout
- FIX-08: CudaDeviceProp replaced with heap buffer (was UB-prone padding)
- FIX-09: Tokenizer byte_fallback to <0xNN> tokens (was panic)

Feature additions:
- FIX-10: SSE streaming (/v1/chat/completions, OpenAI-compatible)
- FIX-11: Correct usage statistics (prompt/completion/total tokens)
- FIX-13: Temperature / top-k / top-p sampling with SamplingParams

Performance improvements:
- FIX-07: Caching allocator wired up (thread-local pool, pooled flag)
- FIX-12: KV cache staging buffers (zero-alloc get_kv_len via borrow_raw)
- FIX-14: GPU strided copy kernel (eliminates contiguous() CPU round-trip)

Architecture:
- Phase 12 engine restructured: prefill/decode separation, honest TODO
  for batched GPU forward (requires Flash Attention)
- Phase 14 updated: FA2 for SM120 (FA4 requires TMEM, absent on 5090)
- Qwen3-7B → Qwen3-8B typo fixed across all docs (36 layers, hidden 4096)

Validated on dash5 (8x RTX 5090):
- 52/52 API prompts pass (EN/CN/code), SSE streaming verified
- Logits match HF transformers 9/10 top-1, 4.0/5 avg top-5 overlap
- 8 concurrent requests: 5.99x scheduling speedup (batch_size=4)
- Throughput: 10.3 tok/s (serial), 30% of HF baseline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 17:53:28 +08:00
parent d8493bd70f
commit ee68d3565d
38 changed files with 3012 additions and 259 deletions

196
tools/bench_vs_hf.py Normal file
View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
Benchmark xserv vs HuggingFace transformers on Qwen3-8B.
Measures: prefill latency, decode throughput, end-to-end latency.
Usage:
# xserv server should be running on port 9090
python3 tools/bench_vs_hf.py
"""
import json
import os
import time
import urllib.request
MODEL_DIR = "/opt/wjh/models/qwen3-8b"
XSERV_URL = "http://localhost:9090"
BENCH_PROMPTS = [
# Short prompts (~10 tokens)
("short", "What is gravity?"),
("short", "Hello, how are you?"),
("short", "Explain DNA briefly."),
# Medium prompts (~30 tokens)
("medium", "Write a detailed explanation of how photosynthesis works in plants, including the light and dark reactions."),
("medium", "Describe the process of machine learning training, including forward pass, loss computation, and backpropagation."),
("medium", "Explain the differences between TCP and UDP protocols, including when you would use each one in practice."),
# Longer prompts (~60 tokens)
("long", "You are an expert computer scientist. Please write a comprehensive explanation of how modern GPUs work, including the architecture of streaming multiprocessors, the memory hierarchy from registers to global memory, and how thousands of threads are scheduled concurrently. Include specific technical details."),
("long", "You are a historian specializing in ancient civilizations. Please provide a detailed analysis of the rise and fall of the Roman Empire, covering the key factors that led to its expansion, the political and social structures that sustained it, and the multiple causes that contributed to its eventual decline and collapse."),
]
MAX_TOKENS = 64
def bench_xserv():
"""Benchmark xserv HTTP API."""
print("\n" + "=" * 60)
print("BENCHMARK: xserv (HTTP API, greedy, max_tokens={})".format(MAX_TOKENS))
print("=" * 60)
# Warmup
body = json.dumps({
"model": "qwen3-8b",
"messages": [{"role": "user", "content": "Hi"}],
"max_tokens": 8,
"temperature": 0.0,
}).encode()
req = urllib.request.Request(
f"{XSERV_URL}/v1/chat/completions",
data=body, headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=120)
print("Warmup done.\n")
results = []
for category, prompt in BENCH_PROMPTS:
body = json.dumps({
"model": "qwen3-8b",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": MAX_TOKENS,
"temperature": 0.0,
}).encode()
req = urllib.request.Request(
f"{XSERV_URL}/v1/chat/completions",
data=body, headers={"Content-Type": "application/json"},
)
t0 = time.perf_counter()
resp = urllib.request.urlopen(req, timeout=300)
elapsed = time.perf_counter() - t0
data = json.loads(resp.read())
usage = data.get("usage", {})
pt = usage.get("prompt_tokens", 0)
ct = usage.get("completion_tokens", 0)
tok_per_sec = ct / elapsed if elapsed > 0 else 0
print(f" [{category:>6}] pt={pt:3d} ct={ct:2d} | {elapsed:6.2f}s | {tok_per_sec:5.1f} tok/s | {prompt[:50]}...")
results.append({
"category": category,
"prompt_tokens": pt,
"completion_tokens": ct,
"elapsed": elapsed,
"tok_per_sec": tok_per_sec,
})
# Summary
total_ct = sum(r["completion_tokens"] for r in results)
total_time = sum(r["elapsed"] for r in results)
avg_tok_per_sec = total_ct / total_time if total_time > 0 else 0
print(f"\n xserv total: {total_ct} tokens in {total_time:.2f}s = {avg_tok_per_sec:.1f} tok/s")
return results
def bench_hf():
"""Benchmark HuggingFace transformers generate()."""
print("\n" + "=" * 60)
print("BENCHMARK: HuggingFace transformers (greedy, max_new_tokens={})".format(MAX_TOKENS))
print("=" * 60)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
print(f"Loading model on GPU 1...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR, dtype=torch.bfloat16, device_map="cuda:1", trust_remote_code=True)
model.eval()
print("Model loaded.\n")
# Warmup
inputs = tokenizer("Hi", return_tensors="pt").to(model.device)
with torch.no_grad():
model.generate(**inputs, max_new_tokens=8, do_sample=False)
print("Warmup done.\n")
results = []
for category, prompt in BENCH_PROMPTS:
# Apply chat template (same as xserv)
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
pt = inputs["input_ids"].shape[1]
torch.cuda.synchronize()
t0 = time.perf_counter()
with torch.no_grad():
output = model.generate(
**inputs,
max_new_tokens=MAX_TOKENS,
do_sample=False,
)
torch.cuda.synchronize()
elapsed = time.perf_counter() - t0
ct = output.shape[1] - pt
tok_per_sec = ct / elapsed if elapsed > 0 else 0
print(f" [{category:>6}] pt={pt:3d} ct={ct:2d} | {elapsed:6.2f}s | {tok_per_sec:5.1f} tok/s | {prompt[:50]}...")
results.append({
"category": category,
"prompt_tokens": pt,
"completion_tokens": ct,
"elapsed": elapsed,
"tok_per_sec": tok_per_sec,
})
total_ct = sum(r["completion_tokens"] for r in results)
total_time = sum(r["elapsed"] for r in results)
avg_tok_per_sec = total_ct / total_time if total_time > 0 else 0
print(f"\n HF total: {total_ct} tokens in {total_time:.2f}s = {avg_tok_per_sec:.1f} tok/s")
del model
torch.cuda.empty_cache()
return results
def main():
xserv_results = bench_xserv()
hf_results = bench_hf()
print("\n" + "=" * 60)
print("COMPARISON SUMMARY")
print("=" * 60)
print(f"\n{'Category':<10} {'Metric':<20} {'xserv':>10} {'HF':>10} {'Ratio':>10}")
print("-" * 62)
for cat in ["short", "medium", "long"]:
xs = [r for r in xserv_results if r["category"] == cat]
hf = [r for r in hf_results if r["category"] == cat]
if xs and hf:
xs_avg_tps = sum(r["tok_per_sec"] for r in xs) / len(xs)
hf_avg_tps = sum(r["tok_per_sec"] for r in hf) / len(hf)
xs_avg_lat = sum(r["elapsed"] for r in xs) / len(xs)
hf_avg_lat = sum(r["elapsed"] for r in hf) / len(hf)
ratio_tps = xs_avg_tps / hf_avg_tps if hf_avg_tps > 0 else 0
ratio_lat = xs_avg_lat / hf_avg_lat if hf_avg_lat > 0 else 0
print(f"{cat:<10} {'Throughput (tok/s)':<20} {xs_avg_tps:>10.1f} {hf_avg_tps:>10.1f} {ratio_tps:>9.2f}x")
print(f"{'':<10} {'Latency (s)':<20} {xs_avg_lat:>10.2f} {hf_avg_lat:>10.2f} {ratio_lat:>9.2f}x")
xs_total_tps = sum(r["completion_tokens"] for r in xserv_results) / sum(r["elapsed"] for r in xserv_results)
hf_total_tps = sum(r["completion_tokens"] for r in hf_results) / sum(r["elapsed"] for r in hf_results)
ratio = xs_total_tps / hf_total_tps if hf_total_tps > 0 else 0
print("-" * 62)
print(f"{'OVERALL':<10} {'Throughput (tok/s)':<20} {xs_total_tps:>10.1f} {hf_total_tps:>10.1f} {ratio:>9.2f}x")
print(f"\nxserv is {ratio:.1%} of HF transformers throughput")
if __name__ == "__main__":
main()

115
tools/compare_logits.py Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Compare xserv prefill logits with HuggingFace transformers on 10 prompts."""
import os
import sys
import subprocess
import re
MODEL_DIR = "/opt/wjh/models/qwen3-8b"
TOP_K = 10
PROMPTS = [
"What is the capital of France?",
"Explain quantum computing.",
"Hello world",
"def fibonacci(n):",
"The weather today is",
"1 + 1 =",
"Machine learning is",
"Once upon a time",
"Paris is known for",
"How does gravity work?",
]
def get_hf_topk(prompt, tokenizer, model, k=10):
import torch
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits[0, -1, :].float().cpu()
topk = torch.topk(logits, k)
return list(zip(topk.indices.tolist(), topk.values.tolist()))
def get_xserv_topk(prompt, k=10):
xserv_bin = "/opt/wjh/projects/xserv/target/release/dump-logits"
env = {**os.environ, "CUDA_VISIBLE_DEVICES": "0",
"PATH": "/usr/local/cuda-12.9/bin:" + os.environ.get("PATH", "")}
result = subprocess.run(
[xserv_bin, MODEL_DIR, prompt],
capture_output=True, text=True, timeout=180, env=env,
)
# Parse output: " [ 0] id= 3555 logit= 24.5000 token=..."
topk = []
for line in result.stdout.strip().split('\n'):
m = re.match(r'\s*\[\s*\d+\]\s+id=\s*(\d+)\s+logit=\s*([\d.\-]+)', line)
if m:
topk.append((int(m.group(1)), float(m.group(2))))
if len(topk) >= k:
break
return topk
def main():
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
print(f"Loading HF model on GPU 1...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR, dtype=torch.bfloat16, device_map="cuda:1", trust_remote_code=True)
model.eval()
print("HF model loaded.\n")
total = len(PROMPTS)
top1_matches = 0
top5_overlaps = []
for i, prompt in enumerate(PROMPTS):
print(f"[{i+1}/{total}] \"{prompt}\"")
hf_top = get_hf_topk(prompt, tokenizer, model, TOP_K)
xs_top = get_xserv_topk(prompt, TOP_K)
if not xs_top:
print(" xserv: NO OUTPUT")
continue
hf_ids = [t[0] for t in hf_top]
xs_ids = [t[0] for t in xs_top]
top1_match = hf_ids[0] == xs_ids[0]
if top1_match:
top1_matches += 1
top5_overlap = len(set(hf_ids[:5]) & set(xs_ids[:5]))
top5_overlaps.append(top5_overlap)
# Show comparison
hf_tok = tokenizer.decode([hf_ids[0]])
xs_tok = tokenizer.decode([xs_ids[0]])
status = "MATCH" if top1_match else "DIFF"
print(f" Top-1: HF={hf_ids[0]:>6}({hf_tok!r:>10}) | xserv={xs_ids[0]:>6}({xs_tok!r:>10}) [{status}]")
print(f" Top-5 overlap: {top5_overlap}/5")
# Show top-5 side by side
print(f" {'HF':>25} | {'xserv':>25}")
for j in range(min(5, len(hf_top), len(xs_top))):
h_id, h_val = hf_top[j]
x_id, x_val = xs_top[j]
h_tok = tokenizer.decode([h_id])
x_tok = tokenizer.decode([x_id])
print(f" {h_id:>6} {h_val:>8.3f} {h_tok!r:>8} | {x_id:>6} {x_val:>8.3f} {x_tok!r:>8}")
print()
print("=" * 50)
print(f"Top-1 match rate: {top1_matches}/{total} ({100*top1_matches/total:.0f}%)")
avg_overlap = sum(top5_overlaps) / max(len(top5_overlaps), 1)
print(f"Avg top-5 overlap: {avg_overlap:.1f}/5")
print(f"Verdict: {'PASS' if top1_matches >= total * 0.7 else 'FAIL'}")
if __name__ == "__main__":
main()

394
tools/e2e_validate.py Normal file
View File

@@ -0,0 +1,394 @@
#!/usr/bin/env python3
"""
End-to-end validation for xserv after bug fixes.
1. Correctness: compare top-k logits with HuggingFace transformers
2. Generation: run 50+ prompts through the HTTP API
3. Performance: measure latency and throughput
Usage:
# Step 1: Start xserv server in background:
# ./target/release/xserv-server /opt/wjh/models/qwen3-8b --port 8080
#
# Step 2: Run this script:
# python3 tools/e2e_validate.py --mode all
# python3 tools/e2e_validate.py --mode logits # correctness only
# python3 tools/e2e_validate.py --mode api # API + perf only
"""
import argparse
import json
import time
import subprocess
import sys
import os
from pathlib import Path
MODEL_DIR = "/opt/wjh/models/qwen3-8b"
XSERV_URL = "http://localhost:8080"
TOP_K = 10
# 50+ diverse test prompts
TEST_PROMPTS = [
"What is the capital of France?",
"Explain quantum computing in simple terms.",
"Write a Python function to sort a list.",
"你好,请用中文介绍一下你自己。",
"What is 2 + 2?",
"The theory of relativity states that",
"In a far away galaxy,",
"def fibonacci(n):",
"请解释什么是机器学习。",
"How does photosynthesis work?",
"What are the benefits of exercise?",
"Once upon a time in a small village,",
"The most important invention of the 20th century was",
"Translate 'hello world' to Japanese.",
"What is the meaning of life?",
"Describe the process of making bread.",
"Why is the sky blue?",
"What is the difference between AI and ML?",
"如何评价GPT-4",
"Write a haiku about autumn.",
"Explain the Pythagorean theorem.",
"What causes earthquakes?",
"How does the internet work?",
"What is the speed of light?",
"Describe the water cycle.",
"What is democracy?",
"How do vaccines work?",
"What is blockchain technology?",
"Explain supply and demand.",
"What is the Big Bang theory?",
"How do airplanes fly?",
"What is climate change?",
"Describe the human digestive system.",
"What is artificial intelligence?",
"How does electricity work?",
"What is the solar system?",
"Explain the concept of gravity.",
"What is DNA?",
"How do computers store data?",
"What is the greenhouse effect?",
"Describe the structure of an atom.",
"What is machine learning?",
"How does Wi-Fi work?",
"What is the stock market?",
"Explain natural selection.",
"What is renewable energy?",
"How do batteries work?",
"What is the United Nations?",
"Describe the process of evolution.",
"What is cryptography?",
"请用三句话总结量子力学的核心概念。",
"用Python写一个计算斐波那契数列的函数。",
]
def logits_correctness_test():
"""Compare xserv prefill logits with HuggingFace transformers."""
print("\n" + "=" * 60)
print("CORRECTNESS TEST: Comparing logits with HuggingFace")
print("=" * 60)
try:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
except ImportError:
print("SKIP: transformers/torch not installed")
return None
print(f"Loading HF model from {MODEL_DIR}...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR,
torch_dtype=torch.bfloat16,
device_map="cuda:1", # Use GPU 1 (xserv uses GPU 0)
trust_remote_code=True,
)
model.eval()
test_prompts = TEST_PROMPTS[:10] # Use first 10 for logits comparison
xserv_bin = "/opt/wjh/projects/xserv/target/release/dump-logits"
results = []
for i, prompt in enumerate(test_prompts):
print(f"\n[{i+1}/{len(test_prompts)}] Prompt: {prompt[:50]}...")
# --- HuggingFace ---
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model(**inputs)
hf_logits = outputs.logits[0, -1, :].float().cpu()
hf_top = torch.topk(hf_logits, TOP_K)
hf_ids = hf_top.indices.tolist()
hf_vals = hf_top.values.tolist()
# --- xserv ---
try:
result = subprocess.run(
[xserv_bin, MODEL_DIR, prompt],
capture_output=True, text=True, timeout=120,
env={**os.environ, "CUDA_VISIBLE_DEVICES": "0",
"PATH": "/usr/local/cuda-12.9/bin:" + os.environ.get("PATH", "")},
)
xserv_lines = [l for l in result.stdout.strip().split('\n') if l.strip().startswith('[')]
xserv_top = []
for line in xserv_lines[:TOP_K]:
parts = line.strip().split()
tid = int([p for p in parts if p.startswith('id=')][0].split('=')[1])
val = float([p for p in parts if p.startswith('logit=')][0].split('=')[1])
xserv_top.append((tid, val))
except Exception as e:
print(f" xserv FAILED: {e}")
results.append({"prompt": prompt, "match": False, "error": str(e)})
continue
# --- Compare ---
xserv_ids = [t[0] for t in xserv_top]
xserv_vals = [t[1] for t in xserv_top]
# Top-1 match
top1_match = hf_ids[0] == xserv_ids[0] if xserv_ids else False
# Top-5 overlap
top5_overlap = len(set(hf_ids[:5]) & set(xserv_ids[:5]))
# Max logit difference for matching tokens
max_diff = 0
for j, (hid, hval) in enumerate(zip(hf_ids[:5], hf_vals[:5])):
for xid, xval in xserv_top[:5]:
if hid == xid:
max_diff = max(max_diff, abs(hval - xval))
hf_tok = tokenizer.decode([hf_ids[0]])
xs_tok = tokenizer.decode([xserv_ids[0]]) if xserv_ids else "???"
status = "PASS" if top1_match else "WARN"
print(f" Top-1: HF={hf_ids[0]}({hf_tok!r}) vs xserv={xserv_ids[0]}({xs_tok!r}) → {status}")
print(f" Top-5 overlap: {top5_overlap}/5, max logit diff: {max_diff:.4f}")
results.append({
"prompt": prompt[:50],
"top1_match": top1_match,
"top5_overlap": top5_overlap,
"max_logit_diff": max_diff,
"hf_top1": f"{hf_ids[0]}({hf_tok})",
"xserv_top1": f"{xserv_ids[0]}({xs_tok})" if xserv_ids else "???",
})
# Summary
print("\n" + "-" * 40)
top1_matches = sum(1 for r in results if r.get("top1_match"))
avg_overlap = sum(r.get("top5_overlap", 0) for r in results) / max(len(results), 1)
print(f"Top-1 match: {top1_matches}/{len(results)}")
print(f"Avg top-5 overlap: {avg_overlap:.1f}/5")
print(f"Verdict: {'PASS' if top1_matches >= len(results) * 0.8 else 'FAIL'}")
# Cleanup
del model
torch.cuda.empty_cache()
return results
def api_generation_test():
"""Test 50+ prompts through the HTTP API."""
print("\n" + "=" * 60)
print("API GENERATION TEST: 50+ prompts via /v1/chat/completions")
print("=" * 60)
import urllib.request
import urllib.error
# Health check
try:
req = urllib.request.Request(f"{XSERV_URL}/health")
resp = urllib.request.urlopen(req, timeout=5)
assert resp.read().decode() == "ok"
print("Health check: OK")
except Exception as e:
print(f"FAIL: Server not reachable at {XSERV_URL}: {e}")
print("Start the server first: ./target/release/xserv-server /opt/wjh/models/qwen3-8b")
return None
# Models endpoint
try:
req = urllib.request.Request(f"{XSERV_URL}/v1/models")
resp = urllib.request.urlopen(req, timeout=5)
models = json.loads(resp.read())
print(f"Models: {[m['id'] for m in models['data']]}")
except Exception as e:
print(f"WARN: /v1/models failed: {e}")
results = []
total_prompt_tokens = 0
total_completion_tokens = 0
total_latency = 0
failures = 0
for i, prompt in enumerate(TEST_PROMPTS):
body = json.dumps({
"model": "qwen3-8b",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 32,
"temperature": 0.0,
}).encode()
try:
req = urllib.request.Request(
f"{XSERV_URL}/v1/chat/completions",
data=body,
headers={"Content-Type": "application/json"},
)
t0 = time.time()
resp = urllib.request.urlopen(req, timeout=120)
latency = time.time() - t0
data = json.loads(resp.read())
content = data["choices"][0]["message"]["content"]
finish = data["choices"][0]["finish_reason"]
usage = data.get("usage", {})
pt = usage.get("prompt_tokens", 0)
ct = usage.get("completion_tokens", 0)
total_prompt_tokens += pt
total_completion_tokens += ct
total_latency += latency
# Basic quality checks
has_content = len(content.strip()) > 0
reasonable_length = ct > 0
status = "OK" if has_content and reasonable_length else "WARN"
if not has_content:
status = "FAIL"
failures += 1
truncated = content[:60].replace('\n', ' ')
print(f" [{i+1:2d}/{len(TEST_PROMPTS)}] {status} | {latency:5.2f}s | pt={pt:3d} ct={ct:2d} | {truncated}...")
results.append({
"prompt": prompt[:40],
"status": status,
"latency": latency,
"prompt_tokens": pt,
"completion_tokens": ct,
"finish_reason": finish,
"content_preview": content[:80],
})
except Exception as e:
print(f" [{i+1:2d}/{len(TEST_PROMPTS)}] FAIL | {e}")
failures += 1
results.append({"prompt": prompt[:40], "status": "FAIL", "error": str(e)})
# Summary
successes = len(results) - failures
avg_latency = total_latency / max(successes, 1)
tok_per_sec = total_completion_tokens / max(total_latency, 0.001)
print("\n" + "-" * 40)
print(f"Results: {successes}/{len(TEST_PROMPTS)} succeeded, {failures} failed")
print(f"Total prompt tokens: {total_prompt_tokens}")
print(f"Total completion tokens: {total_completion_tokens}")
print(f"Average latency: {avg_latency:.2f}s per request")
print(f"Throughput: {tok_per_sec:.1f} tokens/s (completion only)")
print(f"Verdict: {'PASS' if failures <= 2 else 'FAIL'}")
return results
def streaming_test():
"""Test SSE streaming works correctly."""
print("\n" + "=" * 60)
print("STREAMING TEST: SSE /v1/chat/completions?stream=true")
print("=" * 60)
import urllib.request
import urllib.error
body = json.dumps({
"model": "qwen3-8b",
"messages": [{"role": "user", "content": "Count from 1 to 5."}],
"max_tokens": 32,
"temperature": 0.0,
"stream": True,
}).encode()
req = urllib.request.Request(
f"{XSERV_URL}/v1/chat/completions",
data=body,
headers={"Content-Type": "application/json"},
)
try:
resp = urllib.request.urlopen(req, timeout=60)
content_type = resp.headers.get("content-type", "")
print(f"Content-Type: {content_type}")
chunks = []
full_text = ""
has_role_chunk = False
has_done = False
has_finish = False
for line in resp:
line = line.decode().strip()
if not line:
continue
if line.startswith("data: "):
data = line[6:]
if data == "[DONE]":
has_done = True
chunks.append("[DONE]")
continue
try:
obj = json.loads(data)
delta = obj["choices"][0]["delta"]
fr = obj["choices"][0].get("finish_reason")
if "role" in delta:
has_role_chunk = True
if "content" in delta:
full_text += delta["content"]
if fr is not None:
has_finish = True
chunks.append(delta)
except json.JSONDecodeError:
print(f" WARN: bad JSON: {data[:80]}")
print(f"Chunks received: {len(chunks)}")
print(f"Has role chunk: {has_role_chunk}")
print(f"Has finish_reason: {has_finish}")
print(f"Has [DONE]: {has_done}")
print(f"Full text: {full_text[:100]!r}")
ok = has_role_chunk and has_done and has_finish and len(full_text) > 0
# SSE content-type check
if "text/event-stream" in content_type:
print("Content-Type: OK (text/event-stream)")
else:
print(f"WARN: Expected text/event-stream, got {content_type}")
print(f"Verdict: {'PASS' if ok else 'FAIL'}")
return ok
except Exception as e:
print(f"FAIL: {e}")
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["all", "logits", "api", "stream"], default="all")
args = parser.parse_args()
if args.mode in ("all", "logits"):
logits_correctness_test()
if args.mode in ("all", "api"):
api_generation_test()
if args.mode in ("all", "stream"):
streaming_test()
if __name__ == "__main__":
main()

View File

@@ -1,107 +1,66 @@
"""
Test concurrent request handling.
Sends N requests simultaneously, verifies they all produce tokens concurrently.
Usage: python3 tools/test_concurrent.py <server_url> [num_requests]
"""
import sys
import time
#!/usr/bin/env python3
"""Test concurrent requests to verify continuous batching scheduling."""
import json
import threading
import time
import urllib.request
import urllib.error
import concurrent.futures
URL = "http://localhost:9090/v1/chat/completions"
def send_request(url, prompt, max_tokens, results, idx):
"""Send a chat completion request and record timing."""
PROMPTS = [
"What is 1+1?",
"What is 2+2?",
"What is 3+3?",
"What is 4+4?",
"What is 5+5?",
"What is 6+6?",
"What is 7+7?",
"What is 8+8?",
]
def send_request(prompt, idx):
body = json.dumps({
"model": "qwen3-8b",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens,
"max_tokens": 32,
"temperature": 0.0,
}).encode()
req = urllib.request.Request(
f"{url}/v1/chat/completions",
data=body,
headers={"Content-Type": "application/json"},
)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=120) as resp:
data = json.loads(resp.read())
t1 = time.time()
content = data["choices"][0]["message"]["content"]
results[idx] = {
"status": "ok",
"content": content,
"duration_s": t1 - t0,
"finish_reason": data["choices"][0]["finish_reason"],
}
except Exception as e:
t1 = time.time()
results[idx] = {"status": "error", "error": str(e), "duration_s": t1 - t0}
req = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"})
t0 = time.perf_counter()
resp = urllib.request.urlopen(req, timeout=120)
elapsed = time.perf_counter() - t0
data = json.loads(resp.read())
content = data["choices"][0]["message"]["content"][:50].replace('\n', ' ')
ct = data["usage"]["completion_tokens"]
return idx, prompt, elapsed, ct, content
def main():
url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:9090"
n = int(sys.argv[2]) if len(sys.argv) > 2 else 3
max_tokens = 10
print("=== Concurrent request test (8 requests, max_batch=4) ===\n")
prompts = [
"What is the capital of France?",
"Tell me about quantum computing",
"How do airplanes fly?",
"What is machine learning?",
"Explain gravity in simple terms",
][:n]
# Fire all 8 requests concurrently
t_start = time.perf_counter()
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
futures = [pool.submit(send_request, p, i) for i, p in enumerate(PROMPTS)]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
t_total = time.perf_counter() - t_start
print(f"Sending {n} concurrent requests to {url} (max_tokens={max_tokens})")
print("=" * 70)
results.sort(key=lambda r: r[0])
total_tokens = 0
for idx, prompt, elapsed, ct, content in results:
total_tokens += ct
print(f" [{idx}] {elapsed:5.2f}s | ct={ct:2d} | {prompt} -> {content}...")
results = [None] * n
threads = []
t_start = time.time()
for i, prompt in enumerate(prompts):
t = threading.Thread(target=send_request, args=(url, prompt, max_tokens, results, i))
threads.append(t)
t.start()
for t in threads:
t.join()
t_total = time.time() - t_start
print(f"\n{'#':>2} {'Status':>6} {'Duration':>8} {'Content':<50}")
print("-" * 70)
for i, r in enumerate(results):
if r["status"] == "ok":
content_short = r["content"].replace("\n", " ")[:48]
print(f"{i+1:>2} {'OK':>6} {r['duration_s']:>6.1f}s {content_short}")
else:
print(f"{i+1:>2} {'FAIL':>6} {r['duration_s']:>6.1f}s {r['error'][:48]}")
print("=" * 70)
print(f"Total wall time: {t_total:.1f}s")
# Analyze concurrency
durations = [r["duration_s"] for r in results if r["status"] == "ok"]
if len(durations) >= 2:
sequential_estimate = sum(durations)
actual_wall = t_total
concurrency_ratio = sequential_estimate / actual_wall if actual_wall > 0 else 0
print(f"Sum of individual durations: {sequential_estimate:.1f}s")
print(f"Actual wall time: {actual_wall:.1f}s")
print(f"Concurrency ratio: {concurrency_ratio:.2f}x")
if concurrency_ratio > 1.5:
print("✓ CONCURRENT: requests are being processed in parallel")
else:
print("✗ SERIAL: requests appear to be processed sequentially")
all_ok = all(r["status"] == "ok" for r in results)
print(f"\nAll requests succeeded: {all_ok}")
serial_estimate = sum(r[2] for r in results)
print(f"\n Wall clock: {t_total:.2f}s")
print(f" Sum of individual latencies: {serial_estimate:.2f}s")
print(f" Concurrency speedup: {serial_estimate/t_total:.2f}x (1.0x = no batching)")
print(f" Total tokens: {total_tokens}")
print(f" Throughput: {total_tokens/t_total:.1f} tok/s")
if t_total < serial_estimate * 0.85:
print(f"\n Concurrent scheduling is working (wall < 85% of serial sum)")
else:
print(f"\n Limited concurrency benefit (scheduling correct, GPU still per-seq)")
if __name__ == "__main__":
main()