Anthral Labs — Open weights
Anthral Research.
LoRA adapters fine-tuned with GRPO on top of Qwen/Qwen3.5-9B-Base. Trained on retrieval-grounded forecasting questions derived from GDELT events, with strict leakage controls — see the training datasets on Hugging Face: gdelt-forecast-binary and gdelt-forecast-freeform.
Benchmark · pg-320
Phase 2 outperforms gpt-4o-mini by +5.0 accuracy points on a 320-question politics/geopolitics forecasting set, given identical retrieval and prompt. Reproduce locally: rajatagarwal457/pg320-forecast-eval.
| Model | Acc | Brier | Reward |
|---|---|---|---|
| Anthral Phase 2 (Qwen3.5-9B-Base + LoRA) | 60.9 % | 0.259 | +0.350 |
| Anthral Phase 1 (binary-only LoRA) | 57.5 % | 0.269 | +0.306 |
| Qwen3.5-9B-Base (untrained) | 56.3 % | 0.256 | +0.307 |
| gpt-4o-mini | 55.9 % | 0.278 | +0.281 |
Using the adapters
These are LoRA adapters, not standalone models — they sit on top of Qwen/Qwen3.5-9B-Base and only take effect once attached to it. To use one: download an adapter folder below, load the base model with the peft library, and apply the adapter on top. The model expects a forecasting question plus a short bundle of news articles dated before the question's resolution; that retrieval context is how it was trained, and it matters. Phase 1 is tuned for binary yes/no questions; Phase 2 also handles free-form answers. Inference fits on a single 24 GB GPU at bfloat16.
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3.5-9B-Base", torch_dtype="bfloat16", device_map="auto")
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3.5-9B-Base")
model = PeftModel.from_pretrained(base, "./qwen3.5-9b-gdelt-binary-grpo-phase1")
Serving with vLLM
For higher-throughput inference, serve through vLLM. It loads adapters once at startup and exposes an OpenAI-compatible HTTP API, so existing client code drops in unchanged. Both adapters can be served at the same time and selected per-request by the model field. A single A100 80 GB will comfortably handle dozens of concurrent forecasting calls.
vllm serve Qwen/Qwen3.5-9B-Base \
--enable-lora \
--lora-modules phase1=./qwen3.5-9b-gdelt-binary-grpo-phase1 \
phase2=./qwen3.5-9b-gdelt-mixed-grpo-phase2 \
--max-loras 2 --max-lora-rank 16 \
--dtype bfloat16 --max-model-len 32768
Python · OpenAI-compatible client
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
resp = client.chat.completions.create(
model="phase2", # or "phase1"
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
)
print(resp.choices[0].message.content)
Phase 1 — binary forecasting
qwen3.5-9b-gdelt-binary-grpo-phase1
GRPO on 1,015 binary yes/no forecasting questions. Paranoid retrieval — EXPLICIT and IMPLIED leakage stripped. LoRA r=16, α=32. 500 steps.
- adapter_model.safetensors111.0 MiB
- adapter_config.json1.1 KiB
- README.md5.1 KiB
- tokenizer.json19.1 MiB
- tokenizer_config.json1.2 KiB
- chat_template.jinja7.6 KiB
- processor_config.json1.2 KiB
- training_args.bin7.0 KiB
Phase 2 — mixed binary & free-form
qwen3.5-9b-gdelt-mixed-grpo-phase2
Continued from Phase 1. GRPO on a mixed set of binary and free-form forecasting questions, paranoid retrieval. Same LoRA configuration.
- adapter_model.safetensors111.0 MiB
- adapter_config.json1.1 KiB
- README.md5.1 KiB
- tokenizer.json19.1 MiB
- tokenizer_config.json1.2 KiB
- chat_template.jinja7.6 KiB
- processor_config.json1.2 KiB
- training_args.bin7.0 KiB