robustness.py 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. #!/usr/bin/env python3
  2. """Robustness checks (stdlib only).
  3. mixed: one long JSON-schema constrained generation, with short requests and a
  4. forced tool call admitted while it decodes (the pattern that crashed
  5. Aevonix's async-scheduled vLLM deployment); repeated --rounds times.
  6. needle: exact retrieval of three facts hidden in ~N tokens of filler.
  7. usage: robustness.py mixed [--rounds 3] --out FILE
  8. robustness.py needle [--tokens 131072] --out FILE
  9. """
  10. import argparse, json, random, threading, time, urllib.request
  11. p = argparse.ArgumentParser()
  12. p.add_argument("mode", choices=["mixed", "needle"])
  13. p.add_argument("--base", default="http://127.0.0.1:30000/v1")
  14. p.add_argument("--model", default="mimo-v2.6-pro")
  15. p.add_argument("--rounds", type=int, default=3)
  16. p.add_argument("--tokens", type=int, default=131072)
  17. p.add_argument("--out", required=True)
  18. a = p.parse_args()
  19. def chat(messages, timeout=3600, **kw):
  20. body = {"model": a.model, "messages": messages, "temperature": 0,
  21. "chat_template_kwargs": {"enable_thinking": False}, **kw}
  22. req = urllib.request.Request(a.base + "/chat/completions", json.dumps(body).encode(), {"Content-Type": "application/json"})
  23. t0 = time.time()
  24. r = json.load(urllib.request.urlopen(req, timeout=timeout))
  25. return r, time.time() - t0
  26. def mixed():
  27. schema = {"type": "object", "properties": {"items": {"type": "array", "minItems": 300, "maxItems": 300,
  28. "items": {"type": "integer"}}}, "required": ["items"], "additionalProperties": False}
  29. tools = [{"type": "function", "function": {"name": "lookup_order", "description": "Look up an order by id",
  30. "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}}}]
  31. rounds = []
  32. for rnd in range(a.rounds):
  33. out = {}
  34. def long_job():
  35. r, s = chat([{"role": "user", "content": "Return JSON with key items: the integers 1 to 300 in order."}],
  36. max_tokens=3000, response_format={"type": "json_schema", "json_schema": {"name": "seq", "schema": schema}})
  37. items = json.loads(r["choices"][0]["message"]["content"])["items"]
  38. out["long"] = {"ok": items == list(range(1, 301)), "seconds": round(s, 2), "tokens": r["usage"]["completion_tokens"]}
  39. t = threading.Thread(target=long_job); t.start(); time.sleep(3)
  40. shorts = []
  41. for i, (x, y) in enumerate([(17, 25), (123, 456), (9, 91)]):
  42. r, s = chat([{"role": "user", "content": f"What is {x} + {y}? Reply with the number only."}], max_tokens=16)
  43. shorts.append({"ok": str(x + y) in r["choices"][0]["message"]["content"], "seconds": round(s, 2)})
  44. time.sleep(1)
  45. r, s = chat([{"role": "user", "content": "Where is my order A-7731?"}], tools=tools,
  46. tool_choice={"type": "function", "function": {"name": "lookup_order"}}, max_tokens=128)
  47. tc = r["choices"][0]["message"].get("tool_calls") or []
  48. tool_ok = bool(tc) and "A-7731" in tc[0]["function"]["arguments"]
  49. t.join()
  50. rounds.append({"round": rnd, "long": out.get("long"), "short": shorts, "tool_call_ok": tool_ok})
  51. print(json.dumps(rounds[-1]), flush=True)
  52. ok = all(r["long"] and r["long"]["ok"] and r["tool_call_ok"] and all(s["ok"] for s in r["short"]) for r in rounds)
  53. return {"ok": ok, "rounds": rounds}
  54. def needle():
  55. rnd = random.Random(a.tokens)
  56. words = "amber basin cedar delta ember fjord garnet harbor iris juniper kestrel lumen meadow nimbus orchid".split()
  57. facts = {"vault code": str(rnd.randint(100000, 999999)), "courier name": "Tamsin Okonkwo-Reyes",
  58. "ship name": "Halcyon Ferrule"}
  59. n_words = int(a.tokens / 4.6) # "word123" filler is ~4.6 tokens per word
  60. body = [f"{rnd.choice(words)}{rnd.randint(0, 999)}" for _ in range(n_words)]
  61. for i, (k, v) in enumerate(facts.items()):
  62. pos = int(n_words * (0.15 + 0.35 * i))
  63. body.insert(pos, f". IMPORTANT FACT: the {k} is {v}. ")
  64. prompt = (" ".join(body) + "\n\nFrom the text above, what are the vault code, the courier name and the ship name? "
  65. "Answer as three lines: vault code: ..., courier name: ..., ship name: ...")
  66. r, s = chat([{"role": "user", "content": prompt}], max_tokens=128)
  67. ans = r["choices"][0]["message"]["content"]
  68. hits = {k: v in ans for k, v in facts.items()}
  69. return {"ok": all(hits.values()), "prompt_tokens": r["usage"]["prompt_tokens"], "seconds": round(s, 2),
  70. "hits": hits, "answer": ans}
  71. res = mixed() if a.mode == "mixed" else needle()
  72. json.dump(res, open(a.out, "w"), indent=1)
  73. print(json.dumps({k: v for k, v in res.items() if k != "rounds"}))