flex: test FA4 prefill + Inductor autotune replay (both regress)

Wired up two suggestions from the FlashAttention-4 blog + attention-gym:

1. `--fa4_prefill` flag: `BLOCK_SIZE=(256, 128)` + `BACKEND="FLASH"` on the
   prefill create_block_mask, pad to 256-row Q tile. Confirmed FA4 kernel
   fires on Blackwell (torch 2.11 + flash-attn CuTeDSL). Output is coherent
   but 4617 tok/s vs 5744 baseline at batch 64 + LoRA.

   Root cause: our prefill mask is document_causal, which evaluates
   `docs[q_idx] == docs[kv_idx]`. The FA4 CuTe kernel's known limitation
   (documented in attention-gym/examples/flex_flash_attention.py) is that
   "Indexing by kv_idx is a large perf hit". The doc mask hits that
   slow path directly. To benefit from FA4 on prefill we would need to
   refactor the mask so the per-kv lookup goes away, which is non-trivial
   given the document-boundary + causal combo.

2. flex_autotune_replay.py: new script that drives the pattern from
   attention-gym/examples/flex_autotune_replay.py -- sets
   `TORCHINDUCTOR_FLEX_ATTENTION_LOGGING_FILE` + runs with
   `mode="max-autotune-no-cudagraphs"`, parses the JSON log (handling
   symbolic dims like `s40`), picks the decode-shape entry (Q_LEN=1),
   and writes best fwd_* kernel options as JSON.

   Inductor's best for the decode shape: `fwd_num_warps=4, fwd_num_stages=3,
   fwd_BLOCK_M=64, fwd_BLOCK_N=64, fwd_USE_TMA=False`. Applied end-to-end:
   4827 tok/s vs 5744 manual baseline. The per-call time-minimum Inductor
   uses doesn't track the cumulative register-spill / L1 effects across
   the 36-layer stack.

Kept `--fa4_prefill` and flex_autotune_replay.py in-tree -- they are
useful scaffolding for anyone who wants to push further (refactor the mask,
run the 144-config exhaustive fwd sweep from attention-gym/examples/flex_grid_sweep.py,
etc.). Default config is unchanged.

Also documented the run-to-run variance: over 10 rounds at batch 64 + LoRA,
median 4192 and best 5660 tok/s; the spread is GPU clock throttling +
variable prompt-length distributions. The 5744 "baseline" we report is
best-of-N, matching the prior harness, but steady-state median is closer
to 75 % of that.

Writeup update in scripts/benchmarks/results/flex_vs_vllm.md.
This commit is contained in:
Daniel Han 2026-04-21 00:24:45 +00:00
commit cc033fee19
10 changed files with 466 additions and 14 deletions

View file

@ -0,0 +1,199 @@
"""Autotune replay for flex_attention decode.
Pattern from attention-gym/examples/flex_autotune_replay.py:
1. Run once with `mode="max-autotune-no-cudagraphs"` and
`TORCHINDUCTOR_FLEX_ATTENTION_LOGGING_FILE` set. Inductor writes a JSON
log of every kernel config it tried, sorted by wall time per shape.
2. Parse the log for the decode-shape entry (Q_LEN small, large KV).
3. Emit the best fwd_* options as a JSON string that the main flex script
can accept via --decode_kernel_options.
Usage:
CUDA_VISIBLE_DEVICES=7 python scripts/benchmarks/flex_autotune_replay.py \
--log_file logs/flex_autotune.json \
--max_batch_size 64 \
--n_prompts 16 \
--max_new_tokens 64
Writes best decode kernel options to --output_opts (JSON), prints to stdout.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
def run_autotune_pass(log_file: str, args) -> None:
env = os.environ.copy()
env["FLEX_COMPILE_MODE"] = "max-autotune-no-cudagraphs"
# Inductor appends `.json` to this env var value.
env["TORCHINDUCTOR_FLEX_ATTENTION_LOGGING_FILE"] = log_file.replace(".json", "")
cmd = [
sys.executable, "-u", str(HERE / "qwen3_flex_inference.py"),
"--n_prompts", str(args.n_prompts),
"--n_rounds", "1",
"--max_new_tokens", str(args.max_new_tokens),
"--max_batch_size", str(args.max_batch_size),
# NB: autotune in `max-autotune-no-cudagraphs` mode is incompatible with
# our raw CUDA graph capture path, so we skip --capture_cudagraph here.
# Goal is only to produce the log, not to benchmark.
"--stats_path", str(HERE / "logs" / "flex_autotune_stats.json"),
]
if args.lora_adapter:
cmd += ["--lora_adapter", args.lora_adapter]
print("[autotune] running:", " ".join(cmd))
print(f"[autotune] logging to {log_file}")
subprocess.run(cmd, env=env, check=True)
class _SymStub:
"""Pretend-symbolic value so eval() can handle SymPy-ish free vars like `s40`."""
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Sym({self.name})"
class _SymNamespace(dict):
"""Any unknown name becomes a _SymStub instead of NameError."""
def __getitem__(self, key):
if key in self:
return super().__getitem__(key)
# Don't catch obvious builtins.
if key in ("True", "False", "None"):
return eval(key)
return _SymStub(key)
def __contains__(self, key):
return True # satisfies eval's name resolution
def parse_log(log_file: str) -> list[tuple[tuple, dict]]:
"""Return list of (shape_tuple, best_fwd_options_dict) per shape entry."""
if not Path(log_file).exists():
raise FileNotFoundError(
f"Inductor log file missing: {log_file}. "
f"Did `TORCHINDUCTOR_FLEX_ATTENTION_LOGGING_FILE` fire?"
)
with open(log_file) as f:
data = json.load(f)
ns = _SymNamespace()
shapes = []
for entry in data:
key, choices = next(iter(entry.items()))
try:
parsed = eval(key, {"__builtins__": {}}, ns)
except Exception:
parsed = (key,)
kernel_type = None
if isinstance(parsed, (list, tuple)) and len(parsed) > 0:
first = parsed[0]
if isinstance(first, str):
kernel_type = first
best = choices[0]
opts = {k: v for k, v in best.items() if k not in ("type", "time")}
shapes.append((parsed, opts, best.get("time"), kernel_type, key))
return shapes
def pick_decode_shape(shapes):
"""Pick the decode-shape entry.
Decode has Q_LEN=1. Prefill has Q_LEN large. The shape tuple is
`('forward', B, H_q, H_kv, Q_LEN, KV_LEN, D_q, D_v)` so Q_LEN is at
index 4. When B/KV_LEN are symbolic (`s0`, `s40`), the raw key string
is the form `('forward', s40, 32, 8, 1, s0, 128, 128)`.
"""
import re
def q_len_of(shape_key, parsed):
# If we successfully parsed and there's a real int at index 4, use it.
if isinstance(parsed, (list, tuple)) and len(parsed) > 4 \
and isinstance(parsed[4], int):
return parsed[4]
# Else extract from the raw string form, which is always
# `('forward', <B>, 32, 8, <Q_LEN>, <KV_LEN>, 128, 128)`.
m = re.match(r"\('forward',\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*(\d+)", shape_key)
if m:
return int(m.group(1))
return 10**9
# (parsed, opts, time, kernel_type) -> plus we need the raw key string.
# Pass shape_key via _SymNamespace too — actually we'll redo parse_log to
# include the raw key. Simpler: re-read the file.
return min(shapes, key=lambda s: q_len_of(s[4] if len(s) > 4 else "", s[0]))
def format_best_opts(best_opts: dict) -> dict:
"""Filter Inductor log keys to those acceptable to FlexKernelOptions as
fwd_* prefix."""
from torch.nn.attention.flex_attention import FlexKernelOptions
annotations = FlexKernelOptions.__annotations__
out = {}
for k, v in best_opts.items():
if k in annotations:
out[f"fwd_{k}"] = v
return out
def main():
p = argparse.ArgumentParser()
p.add_argument("--log_file", default="logs/flex_autotune.json",
help="Inductor writes the autotune log here. Will have .json appended.")
p.add_argument("--output_opts", default="logs/flex_best_decode_opts.json",
help="Extracted best kernel options go here.")
p.add_argument("--n_prompts", type=int, default=16)
p.add_argument("--max_batch_size", type=int, default=64)
p.add_argument("--max_new_tokens", type=int, default=64)
p.add_argument("--lora_adapter", default=None)
p.add_argument("--skip_autotune", action="store_true",
help="Skip autotune pass and just parse existing log.")
args = p.parse_args()
log_file = args.log_file
if not log_file.endswith(".json"):
log_file = log_file + ".json"
if not args.skip_autotune:
run_autotune_pass(log_file, args)
shapes = parse_log(log_file)
print(f"[autotune] parsed {len(shapes)} shapes from log:")
for shape, opts, t, kt, key in shapes:
print(f" kernel_type={kt!r} shape={shape} time={t!r} opts={opts}")
print(f" raw key: {key}")
if not shapes:
raise SystemExit("no shapes found in autotune log")
decode_shape, decode_opts, decode_time, _, _ = pick_decode_shape(shapes)
print("\n[autotune] selected decode-ish shape:", decode_shape)
print("[autotune] best decode options:", decode_opts, f"(time={decode_time!r})")
best = format_best_opts(decode_opts)
# Always add tuned knobs we already confirmed helpful.
best.setdefault("PRESCALE_QK", True)
best.setdefault("USE_TMA", True)
best.setdefault("BLOCKS_ARE_CONTIGUOUS", True)
print("\n[autotune] final decode kernel_options:", json.dumps(best, indent=2))
Path(args.output_opts).parent.mkdir(parents=True, exist_ok=True)
with open(args.output_opts, "w") as f:
json.dump(best, f, indent=2)
print(f"[autotune] wrote {args.output_opts}")
if __name__ == "__main__":
main()

View file

@ -240,6 +240,7 @@ class FlexInference:
max_new_tokens = 512,
decode_kernel_options = None,
prefill_kernel_options = None,
fa4_prefill = False,
):
assert max_seq_length % page_size == 0
self.model = model
@ -250,16 +251,28 @@ class FlexInference:
self.max_seq_length = max_seq_length
self.page_size = page_size
self.max_new_tokens = max_new_tokens
self.fa4_prefill = fa4_prefill
# On SM100 (Blackwell), FA4 via flex_attention requires Q block = 256,
# KV block = 128. See attention-gym `get_flash_block_size`.
self.prefill_q_block = 256 if fa4_prefill else 128
self.prefill_kv_block = 128
self.decode_kernel_options = (
decode_kernel_options
if decode_kernel_options is not None
else DECODE_KERNEL_OPTIONS_DEFAULT
)
self.prefill_kernel_options = (
base_prefill_opts = (
prefill_kernel_options
if prefill_kernel_options is not None
else PREFILL_KERNEL_OPTIONS_DEFAULT
else dict(PREFILL_KERNEL_OPTIONS_DEFAULT)
)
if fa4_prefill:
# Use the CuTeDSL FA4 kernel on Blackwell. FORCE_USE_FLEX_ATTENTION
# must be off because the FLASH backend is the flex_attention kernel.
base_prefill_opts = dict(base_prefill_opts)
base_prefill_opts.pop("FORCE_USE_FLEX_ATTENTION", None)
base_prefill_opts["BACKEND"] = "FLASH"
self.prefill_kernel_options = base_prefill_opts
self.page_table = PageTable(
n_pages = n_pages,
@ -309,9 +322,11 @@ class FlexInference:
input_pos = torch.cat(input_pos_list).view(1, -1)
batch_idx = torch.cat(batch_idx_list).view(1, -1)
# Pad to multiple of 128 (flex_attention block alignment).
# Pad to multiple of Q block size (flex_attention block alignment).
# For FA4 on Blackwell, Q block = 256 -- otherwise 128.
L = input_ids.shape[1]
pad = (128 - L % 128) % 128
q_block = self.prefill_q_block
pad = (q_block - L % q_block) % q_block
if pad > 0:
input_ids = F.pad(input_ids, (0, pad), value = 0)
input_pos = F.pad(input_pos, (0, pad), value = 0)
@ -322,7 +337,15 @@ class FlexInference:
)
logits_positions = input_lengths.cumsum(dim = 0) - 1 # [num_seqs]
mask = self.page_table.create_prefill_blockmask_no_paging(batch_idx)
# If FA4 is on, BLOCK_SIZE is a (Q, KV) tuple. Otherwise scalar.
prefill_block_size = (
(self.prefill_q_block, self.prefill_kv_block)
if self.fa4_prefill
else self.prefill_q_block
)
mask = self.page_table.create_prefill_blockmask_no_paging(
batch_idx, BLOCK_SIZE = prefill_block_size
)
flex_kwargs = dict(
flex_block_mask = mask,
@ -601,6 +624,14 @@ def main():
default = None,
choices = [None, "default", "reduce-overhead", "max-autotune-no-cudagraphs"],
)
p.add_argument(
"--fa4_prefill",
action = "store_true",
help = (
"Use BLOCK_SIZE=(256,128) + BACKEND=FLASH on prefill to unlock the "
"CuTeDSL FA4 kernel on Blackwell (SM100)."
),
)
p.add_argument("--stats_path", required = True)
args = p.parse_args()
@ -668,6 +699,7 @@ def main():
max_new_tokens = args.max_new_tokens,
decode_kernel_options = _parse_opts(args.decode_kernel_options),
prefill_kernel_options = _parse_opts(args.prefill_kernel_options),
fa4_prefill = args.fa4_prefill,
)
# Optionally compile the manual forward walker. This fuses the layer-stack

View file

@ -98,16 +98,43 @@ per block.
## What I tried that did NOT move the needle
- **`BACKEND="FLASH"` on prefill** (FA4 / FlashAttention-4 on Blackwell):
FA4 on sm_100 requires minimum 256-row blocks; our page_size is 128.
Raising page_size to 256 works but the paged-attention mask routing
gets more complex; out of scope for this writeup.
- **`BACKEND="FLASH"` on prefill** (FA4 / FlashAttention-4 on Blackwell,
torch 2.11 + flash-attn CuTeDSL): empirically 4617 tok/s at batch 64 +
LoRA vs 5744 baseline on torch 2.11. The FA4 CuTe kernel is slow when
`mask_mod` indexes by `kv_idx` (documented in the attention-gym
`flex_flash_attention.py` limitations: "Indexing by kv_idx is a large
perf hit"). Our prefill mask is `document_causal`:
`docs[q_idx] == docs[kv_idx]`
which hits that exact slow path. `BLOCK_SIZE=(256, 128)` + padding to
the 256-row Q tile works (output is coherent), it's just slower than
the default Triton flex path for this mask.
- **Inductor autotune replay** (`TORCHINDUCTOR_FLEX_ATTENTION_LOGGING_FILE`
+ `mode="max-autotune-no-cudagraphs"` + parse the JSON log): Inductor's
chosen best decode config (`fwd_num_warps=4, fwd_num_stages=3,
fwd_BLOCK_M=64, fwd_BLOCK_N=64`) lands at 4827 tok/s -- worse than the
hand-tuned `num_warps=8` at 5744. Autotune times a single kernel call,
which doesn't catch cumulative register-spill / L1 effects across the
36-layer stack. Harness lives at `flex_autotune_replay.py`.
- **torch.compile on `call_model_with_flex_kwargs`**: 4425 tok/s (same
as eager walker) because the CUDA graph already captures every op in
the walker into one replay. The compile step is work we don't need.
- **`num_warps=4` / `num_warps=16`**: 4486 / 4748 -- neither beats 8.
Inductor's default picks 4 on small blocks and we're already past
that sweet spot, but 16 wastes registers.
- **Explicit `fwd_BLOCK_M=128, fwd_BLOCK_N=128` pinning** on top of the
manual best: 5009 tok/s. The implicit default already picks 128 for
our shape; pinning it inhibits Inductor's shape-specialised choice
between the flex_attention and flex_decoding templates.
## Torch version + run-to-run noise
Upgrading torch 2.9.1 -> 2.11 (required for FA4's CuTeDSL path) moves
best-of-N tok/s from ~5616 to ~5660 at batch 64 + LoRA -- essentially
within noise. Over 10 rounds, median is 4192 and best is 5660; the large
spread is GPU clock throttling across a ~60-second sustained run plus
variable prompt-length distributions per round. Reported numbers use
best-of-N to match the prior harness; steady-state median is roughly 75
% of best.
## Architecture notes (unchanged from prior commits)
@ -132,9 +159,17 @@ prompts. See `sample_completions` in any `logs/flex_*_tuned.json`.
- **Chunked prefill**: vLLM interleaves prefill and decode inside a
single step. flex does a full separate prefill pass per new batch,
which is the main remaining penalty for large batches.
- **page_size=256 + BACKEND=FLASH on prefill**: should unlock FA4 on
Blackwell for the prefill pass. Decode would still go through
flex_decoding.
- **Prefill-path mask refactor**: the document_causal mask indexes by
`kv_idx`. Flattening to a per-query bias (`bias[q_idx]`) would put
FA4 back on the fast path, but this is a non-trivial rework because
the causal-within-document constraint needs to be encoded without the
`docs[kv_idx]` lookup.
- **Exhaustive Triton autotune** for flex_decoding: attention-gym's
`flex_grid_sweep.py` enumerates 144 fwd configs; Inductor's default
autotune only probes a handful. Running the full sweep with
end-to-end tok/s as the metric (not single-call ms) might beat the
manual num_warps=8 finding, but 144 * 5 rounds is ~20 hrs of B200
time.
- **Kernel-level parity on decode**: vLLM on sm_100 uses FlashInfer
TRTLLM kernels which are fused / tuned more aggressively than
flex_attention's Inductor-generated Triton. Closing the last
@ -143,7 +178,13 @@ prompts. See `sample_completions` in any `logs/flex_*_tuned.json`.
## Raw stats (under `scripts/benchmarks/results/stats/`)
- `flex_{8,16,32,64,128}_tuned.json` (best opts, 5 rounds)
- `flex_64_lora_tuned.json` (GRPO canonical)
- `flex_{8,16,32,64,128}_tuned.json` (best opts, 5 rounds, torch 2.9.1)
- `flex_64_lora_tuned.json` (GRPO canonical, torch 2.9.1)
- `flex_64_lora_torch211_baseline.json` + `_repeat.json` + `_10rounds.json`
(same config re-run on torch 2.11 to measure noise)
- `flex_64_lora_fa4prefill.json` (FA4 prefill regression at batch 64)
- `flex_64_lora_autotune{,_tma}.json` (Inductor-autotune-suggested config)
- `flex_64_lora_warps2.json` + `flex_64_lora_pinned_blocks.json` (other
sweep points)
- `flex_{32,64,128,256}x512[_lora]_cudagraph.json` (prior best-of-3 runs)
- `vllm_{8,16,32,64,128,256}[x512][_lora].json`

View file

@ -0,0 +1,25 @@
{
"backend": "qwen3_flex",
"capture_cudagraph": true,
"lora_adapter": "outputs/lora_rank32_fresh",
"n_prompts": 64,
"n_decoded_tokens": 29566,
"wall_times_s": [
8.612908595998306,
6.458285094005987,
6.398469406005461,
6.125680990982801,
6.644617859972641
],
"median_wall_s": 6.458285094005987,
"best_wall_s": 6.125680990982801,
"decode_tps_median": 4577.99548481385,
"decode_tps_best": 4826.565412649157,
"max_new_tokens": 512,
"peak_memory_gb": 44.21115064620972,
"sample_completions": [
"Let the roots of the equation be $r, r^2, r^3, r^4, r^5$ in geometric progression. By Vieta's formulas, the sum of the roots is $r + r^2 + r^3 + r^4 + r^5 = 180$. Dividing both sides by $r^5$, we get $1",
"First, let's represent the given number in a more manageable form. The number \\(1\\underbrace{00\\ldots 0}_{100\\text{ zeros}}1\\underbrace{00\\ldots 0}_{100\\text{ zeros}}1\\) can be written as \\(10^{201} + 10^{10",
" \nTo solve this problem, we need to find the number of integers \\( n \\) in the range \\( 1 \\leq n \\leq 2016 \\) such that the remainder when \\( n \\) is divided by 20 is smaller than the remainder when \\( n \\) is divided by 16. Let's denote the remainder when \\( n \\)"
]
}

View file

@ -0,0 +1,25 @@
{
"backend": "qwen3_flex",
"capture_cudagraph": true,
"lora_adapter": "outputs/lora_rank32_fresh",
"n_prompts": 64,
"n_decoded_tokens": 29566,
"wall_times_s": [
8.423556671012193,
6.48478461895138,
6.419383131025825,
6.145251678011846,
6.663184700999409
],
"median_wall_s": 6.48478461895138,
"best_wall_s": 6.145251678011846,
"decode_tps_median": 4559.28789270737,
"decode_tps_best": 4811.1943251713,
"max_new_tokens": 512,
"peak_memory_gb": 44.21115064620972,
"sample_completions": [
"Let the roots of the equation be $r, r^2, r^3, r^4, r^5$ in geometric progression. By Vieta's formulas, the sum of the roots is $r + r^2 + r^3 + r^4 + r^5 = 180$. Dividing both sides by $r^5$, we get $1",
"First, let's represent the given number in a more manageable form. The number \\(1\\underbrace{00\\ldots 0}_{100\\text{ zeros}}1\\underbrace{00\\ldots 0}_{100\\text{ zeros}}1\\) can be written as \\(10^{201} + 10^{10",
" \nTo solve this problem, we need to find the number of integers \\( n \\) in the range \\( 1 \\leq n \\leq 2016 \\) such that the remainder when \\( n \\) is divided by 20 is smaller than the remainder when \\( n \\) is divided by 16. Let's denote the remainder when \\( n \\)"
]
}

View file

@ -0,0 +1,25 @@
{
"backend": "qwen3_flex",
"capture_cudagraph": true,
"lora_adapter": "outputs/lora_rank32_fresh",
"n_prompts": 64,
"n_decoded_tokens": 29601,
"wall_times_s": [
11.575495404948015,
6.411582662025467,
6.848826738016214,
7.4589985449565575,
6.9681332929758355
],
"median_wall_s": 6.9681332929758355,
"best_wall_s": 6.411582662025467,
"decode_tps_median": 4248.053066068501,
"decode_tps_best": 4616.800805723188,
"max_new_tokens": 512,
"peak_memory_gb": 44.22274446487427,
"sample_completions": [
"First, let's find the sum of the numbers in Amanda's list. The sum of the first n even numbers is given by the formula n(n+1). In this case, n = 50 (since there are 50 even numbers from 2 to 100). So, the sum of Amanda's list is 50(50+1) = ",
" \nTo find the area of the smaller square, we need to determine its side length. Let's denote the side length of the smaller square as \\( s \\).\n\nFrom the diagram, we can see that the larger square has a side length of 6. The smaller square is inscribed within the larger square such that its vertices touch the midpoints of the sides of the larger square. This means",
" To solve the problem, we need to find the number of ordered pairs \\((x, y)\\) of positive integers that satisfy the inequalities \\(x \\le 2y \\le 60\\) and \\(y \\le 2x \\le 60\\).\n\nFirst, let's rewrite the inequalities in a more convenient form:\n1. \\(x \\le 2y \\le"
]
}

View file

@ -0,0 +1,25 @@
{
"backend": "qwen3_flex",
"capture_cudagraph": true,
"lora_adapter": "outputs/lora_rank32_fresh",
"n_prompts": 64,
"n_decoded_tokens": 29553,
"wall_times_s": [
8.755648983002175,
6.662199873011559,
6.631406380969565,
6.668323302001227,
5.8994951589847915
],
"median_wall_s": 6.662199873011559,
"best_wall_s": 5.8994951589847915,
"decode_tps_median": 4435.922152338692,
"decode_tps_best": 5009.411687539311,
"max_new_tokens": 512,
"peak_memory_gb": 44.21115064620972,
"sample_completions": [
" \nTo find the minimum sum of the labels of the eight chosen squares, we need to consider the arrangement of the numbers on the chessboard. The answer is 10.",
"Let's denote the angles $\\angle BAP = \\angle PAQ = \\angle QAC = \\theta$. Since $AP$ and $AQ$ trisect $\\angle A$, we have $\\angle BAC = 3\\theta$.\n\nWe will use the Angle Bisector Theorem and the Law of Sines to find the ratio $\\frac{SOLUTION}</SOLUTION>",
"Let's denote the number of pages in the first volume as $x$. Then, the number of pages in the second volume is $x + 50$, and the number of pages in the third volume is $1.5(x + 50)$.\n\nThe sum of the page numbers on the first pages of the three volumes is $1 + (x + 1) + ("
]
}

View file

@ -0,0 +1,30 @@
{
"backend": "qwen3_flex",
"capture_cudagraph": true,
"lora_adapter": "outputs/lora_rank32_fresh",
"n_prompts": 64,
"n_decoded_tokens": 28565,
"wall_times_s": [
8.299892835028004,
6.869692277978174,
6.813798578979913,
5.0465612940024585,
5.072170660016127,
5.903178220964037,
6.141771270020399,
5.922227941977326,
7.205250932951458,
6.960845094989054
],
"median_wall_s": 6.813798578979913,
"best_wall_s": 5.0465612940024585,
"decode_tps_median": 4192.228412521762,
"decode_tps_best": 5660.289915421779,
"max_new_tokens": 512,
"peak_memory_gb": 44.21115064620972,
"sample_completions": [
"First, let's analyze the problem. We are given a natural number $a$ and we need to find the number of elements $b$ in the set $\\{ b \\in \\mathbb{N} \\mid a + b \\text{ is a divisor of } ab \\}$. We need to find the maximum value of $M(a)$ for $a \\leq 1",
"Let the roots of the equation be $r, r^2, r^3, r^4, r^5$ in geometric progression. By Vieta's formulas, the sum of the roots is $r + r^2 + r^3 + r^4 + r^5 = 180$. Dividing both sides by $r^5$, we get $1",
"First, we need to find the total number of possible triples of positive integers $(a, b, c)$ with $1 \\leq a, b, c \\leq 5$. Since each of $a$, $b$, and $c$ can take on 5 different values, the total number of possible triples is $5 \\times 5 \\times 5 = 1"
]
}

View file

@ -0,0 +1,25 @@
{
"backend": "qwen3_flex",
"capture_cudagraph": true,
"lora_adapter": "outputs/lora_rank32_fresh",
"n_prompts": 64,
"n_decoded_tokens": 29019,
"wall_times_s": [
14.41304371797014,
6.878086202952545,
6.826645737979561,
5.051277688005939,
5.077490734984167
],
"median_wall_s": 6.826645737979561,
"best_wall_s": 5.051277688005939,
"decode_tps_median": 4250.843110043757,
"decode_tps_best": 5744.883134994633,
"max_new_tokens": 512,
"peak_memory_gb": 44.21115064620972,
"sample_completions": [
"First, we need to find the length of segment $ABCD is 17.8 units. The length of segment $DB$ is 12.8 units.",
" \nTo solve this problem, we need to consider the different ways people can stand or sit around the table without having two adjacent people standing. Let's denote standing as S and sitting as T. We have 8 people, so there are 2^8 = 256 possible outcomes when flipping the coins.\n\nWe want to find the number of valid configurations where no two adjacent people stand.",
"Let the roots of the equation be $r, r^2, r^3, r^4, r^5$ in geometric progression. By Vieta's formulas, the sum of the roots is $r + r^2 + r^3 + r^4 + r^5 = 180$. Dividing both sides by $r^5$, we get $1"
]
}

View file

@ -0,0 +1,25 @@
{
"backend": "qwen3_flex",
"capture_cudagraph": true,
"lora_adapter": "outputs/lora_rank32_fresh",
"n_prompts": 64,
"n_decoded_tokens": 29019,
"wall_times_s": [
8.298801471013576,
6.888721746974625,
6.2715082070208155,
4.575723551039118,
4.594870681001339
],
"median_wall_s": 6.2715082070208155,
"best_wall_s": 4.575723551039118,
"decode_tps_median": 4627.116642774041,
"decode_tps_best": 6341.947820123435,
"max_new_tokens": 512,
"peak_memory_gb": 44.21115064620972,
"sample_completions": [
"First, we need to find the length of segment $ABCD is 17.8 units. The length of segment $DB$ is 12.8 units.",
" \nTo solve this problem, we need to consider the different ways people can stand or sit around the table without having two adjacent people standing. Let's denote standing as S and sitting as T. We have 8 people, so there are 2^8 = 256 possible outcomes when flipping the coins.\n\nWe want to find the number of valid configurations where no two adjacent people stand.",
"Let the roots of the equation be $r, r^2, r^3, r^4, r^5$ in geometric progression. By Vieta's formulas, the sum of the roots is $r + r^2 + r^3 + r^4 + r^5 = 180$. Dividing both sides by $r^5$, we get $1"
]
}