chore: add standalone qwenapp and session-routing analysis scripts

Three one-off scripts kept outside the two packages: a formatter for the
qwenapp Aquila access-log shape, its characterization data generator, and a
session-sticky routing imbalance simulation over a formatted trace.
This commit is contained in:
2026-09-11 06:31:20 +00:00
parent 03d4828818
commit 8b536cab2e
3 changed files with 752 additions and 0 deletions

269
format_qwenapp_trace.py Normal file
View File

@@ -0,0 +1,269 @@
"""Format raw qwenapp Aquila access-log trace into the standard release format.
qwenapp traces differ from coding-agent traces in two ways:
1. Prompts are pre-rendered strings (not a `messages` list).
2. No tool calls — this is a general chat + RAG workload.
The script reads the raw JSONL, groups rows by session_id, assigns
chat_id / parent_chat_id / turn, tokenizes the prompt to build hash_ids
for prefix-cache replay, and writes the standard format understood by
the replayer and sample_trace.py.
Usage (run on dash1 where the trace and tokenizer live):
python scripts/format_qwenapp_trace.py \
--input ~/ali-trace/trace-qwenapp/0529-1000-1200.jsonl \
--output ~/ali-trace/trace-qwenapp-formatted/0529-1000-1200.jsonl \
--tokenizer ~/ali-trace/trace_model_meta/Qwen/Qwen3-Coder-480B-A35B-Instruct/tokenizer.json
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from array import array
from collections import defaultdict
from pathlib import Path
from tokenizers import Tokenizer
from tqdm.auto import tqdm
def _block_digest(block: list[int]) -> bytes:
digest = hashlib.blake2b(digest_size=16)
digest.update(len(block).to_bytes(4, "little", signed=False))
digest.update(array("I", block).tobytes())
return digest.digest()
def _parse_row(raw: dict) -> dict | None:
"""Extract the fields we need from one raw Aquila access-log row."""
request_params = raw.get("request_params")
if isinstance(request_params, str):
request_params = json.loads(request_params)
response_params = raw.get("response_params")
if isinstance(response_params, str):
response_params = json.loads(response_params)
if not isinstance(request_params, dict) or not isinstance(response_params, dict):
return None
inp = request_params.get("payload", {}).get("input", {})
resp_payload = response_params.get("payload", {})
usage = resp_payload.get("usage", {})
resp_header = response_params.get("header", {})
prompt = inp.get("prompt", "")
if not prompt:
return None
input_tokens = int(usage.get("input_tokens", 0))
output_tokens = int(usage.get("output_tokens", 0))
if input_tokens <= 0:
return None
status_code = str(resp_header.get("status_code", raw.get("status_code", "")))
if status_code and status_code != "200" and status_code != "1000":
return None
# Timestamp: prefer the `time` field (ms precision), fall back to __time__
time_str = raw.get("time", "")
wall_clock_s = _parse_time_to_epoch(time_str) if time_str else None
if wall_clock_s is None:
epoch_s = raw.get("__time__")
wall_clock_s = float(epoch_s) if epoch_s else 0.0
session_id = str(inp.get("session_id", ""))
user_id = str((inp.get("source") or {}).get("user_id", ""))
request_id = str(raw.get("request_id", inp.get("request_id", "")))
return {
"session_id": session_id,
"user_id": user_id,
"request_id": request_id,
"wall_clock_s": wall_clock_s,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"prompt": prompt,
"total_cost_time_ms": int(raw.get("total_cost_time", 0)),
}
def _parse_time_to_epoch(time_str: str) -> float | None:
"""Parse '2026-05-29 09:59:59.505' (UTC+8) to epoch seconds."""
import datetime
try:
dt = datetime.datetime.strptime(time_str.strip(), "%Y-%m-%d %H:%M:%S.%f")
except ValueError:
try:
dt = datetime.datetime.strptime(time_str.strip(), "%Y-%m-%d %H:%M:%S")
except ValueError:
return None
tz_utc8 = datetime.timezone(datetime.timedelta(hours=8))
dt = dt.replace(tzinfo=tz_utc8)
return dt.timestamp()
def main() -> None:
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--input", type=Path, required=True)
p.add_argument("--output", type=Path, required=True)
p.add_argument("--tokenizer", type=Path, required=True,
help="Path to tokenizer.json (Qwen3 family)")
p.add_argument("--block-size", type=int, default=512)
p.add_argument("--ttp", action="store_true",
help="Also emit time_to_parent_chat field")
args = p.parse_args()
print(f"Loading tokenizer from {args.tokenizer} ...")
tokenizer = Tokenizer.from_file(str(args.tokenizer))
# --- Pass 1: read all rows, group by session ---
print(f"Reading {args.input} ...")
sessions: dict[str, list[dict]] = defaultdict(list)
skipped = 0
total = 0
with args.input.open("r", encoding="utf-8") as fh:
for line in tqdm(fh, desc="Read", unit="row"):
total += 1
raw = json.loads(line)
parsed = _parse_row(raw)
if parsed is None:
skipped += 1
continue
sessions[parsed["session_id"]].append(parsed)
print(f" Read {total} rows, kept {total - skipped}, skipped {skipped}")
print(f" Sessions: {len(sessions)}")
# --- Sort each session by timestamp and assign turn/chat_id ---
for rows in sessions.values():
rows.sort(key=lambda r: r["wall_clock_s"])
# Assign globally-unique chat_ids
next_chat_id = 1
chat_id_map: dict[str, int] = {} # request_id -> chat_id
all_rows: list[dict] = []
for sid, rows in sessions.items():
prev_chat_id = -1
prev_end_s = None
for turn_idx, row in enumerate(rows, start=1):
cid = next_chat_id
next_chat_id += 1
chat_id_map[row["request_id"]] = cid
ttp = None
if prev_end_s is not None:
ttp = row["wall_clock_s"] - prev_end_s
if ttp < 0:
ttp = 0.0
all_rows.append({
"session_id": sid,
"chat_id": cid,
"parent_chat_id": prev_chat_id,
"turn": turn_idx,
"wall_clock_s": row["wall_clock_s"],
"input_tokens": row["input_tokens"],
"output_tokens": row["output_tokens"],
"prompt": row["prompt"],
"time_to_parent_chat": ttp,
"total_cost_time_ms": row["total_cost_time_ms"],
})
prev_chat_id = cid
prev_end_s = row["wall_clock_s"] + row["total_cost_time_ms"] / 1000.0
# Sort globally by timestamp
all_rows.sort(key=lambda r: r["wall_clock_s"])
# Re-zero timestamps
t0 = all_rows[0]["wall_clock_s"] if all_rows else 0.0
# --- Pass 2: tokenize and hash ---
print(f"Tokenizing {len(all_rows)} rows (block_size={args.block_size}) ...")
block_ids_by_digest: dict[str, int] = {}
next_block_id = 0
args.output.parent.mkdir(parents=True, exist_ok=True)
with args.output.open("w", encoding="utf-8") as out:
for row in tqdm(all_rows, desc="Tokenize+write", unit="row"):
token_ids = tokenizer.encode(row["prompt"]).ids
# Build block hashes
hash_ids = []
for i in range(0, len(token_ids), args.block_size):
block = token_ids[i:i + args.block_size]
digest_hex = _block_digest(block).hex()
bid = block_ids_by_digest.get(digest_hex)
if bid is None:
bid = next_block_id
next_block_id += 1
block_ids_by_digest[digest_hex] = bid
hash_ids.append(bid)
# Pad hash_ids to match provider input_tokens if needed
target_blocks = (row["input_tokens"] + args.block_size - 1) // args.block_size
while len(hash_ids) < target_blocks:
pad_key = f"pad:{row['chat_id']}:{len(hash_ids)}"
pad_hex = hashlib.blake2b(pad_key.encode(), digest_size=16).hexdigest()
bid = block_ids_by_digest.get(pad_hex)
if bid is None:
bid = next_block_id
next_block_id += 1
block_ids_by_digest[pad_hex] = bid
hash_ids.append(bid)
hash_ids = hash_ids[:target_blocks]
out_row = {
"chat_id": row["chat_id"],
"parent_chat_id": row["parent_chat_id"],
"timestamp": round(row["wall_clock_s"] - t0, 3),
"input_length": row["input_tokens"],
"output_length": row["output_tokens"],
"type": "app",
"turn": row["turn"],
"hash_ids": hash_ids,
"session_id": row["session_id"],
}
if args.ttp and row["time_to_parent_chat"] is not None:
out_row["time_to_parent_chat"] = round(row["time_to_parent_chat"], 3)
out.write(json.dumps(out_row, ensure_ascii=False) + "\n")
# --- Summary ---
n_sessions = len(sessions)
n_requests = len(all_rows)
turns_per = [len(v) for v in sessions.values()]
multi = sum(1 for t in turns_per if t > 1)
span_s = all_rows[-1]["wall_clock_s"] - t0 if all_rows else 0
qps = n_requests / span_s if span_s > 0 else 0
inp_lens = [r["input_tokens"] for r in all_rows]
out_lens = [r["output_tokens"] for r in all_rows]
unique_blocks = next_block_id
shared_blocks = sum(1 for d, bid in block_ids_by_digest.items()
if not d.startswith("pad:")) # rough
# Count actually shared (appear in >1 request)
block_ref_count: dict[int, int] = defaultdict(int)
# Re-scan is expensive; use the hash_ids we just wrote
# Instead, just report unique count
print(f"\n=== Summary ===")
print(f" Requests: {n_requests}")
print(f" Sessions: {n_sessions}")
print(f" Multi-turn: {multi} ({multi*100/n_sessions:.1f}%)")
print(f" Turns/session: min={min(turns_per)} max={max(turns_per)} avg={sum(turns_per)/len(turns_per):.1f}")
print(f" Input tokens: min={min(inp_lens)} max={max(inp_lens)} avg={sum(inp_lens)/len(inp_lens):.0f}")
print(f" Output tokens: min={min(out_lens)} max={max(out_lens)} avg={sum(out_lens)/len(out_lens):.0f}")
print(f" Trace span: {span_s:.1f}s ({span_s/60:.1f} min)")
print(f" QPS: {qps:.2f}")
print(f" Unique hash blocks: {unique_blocks}")
print(f"\nWrote {n_requests} rows to {args.output}")
if __name__ == "__main__":
main()