Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 185 additions & 3 deletions benchmarks/benchmark_ratio_kl.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you post benchmark numbers at low density — say 0.1, 0.25, 0.5, 1.0 — for both latency and peak memory?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — I reran the representative [B, T, V] = [32, 256, 32768] case at densities 0.1, 0.25, 0.5, and 1.0 for both FP16 and BF16.

The table reports base → PR. Latencies are medians in ms; peak is incremental peak allocated memory in MiB.

dtype density isolated backward (ms) forward + backward (ms) incremental peak (MiB)
BF16 0.10 0.9941 → 0.3245 (3.064x) 1.1303 → 0.4493 (-60.3%) 1536.1 → 512.1 (-1024.0)
BF16 0.25 1.0844 → 0.3463 (3.131x) 1.2356 → 0.4942 (-60.0%) 1536.1 → 512.1 (-1024.0)
BF16 0.50 1.2234 → 0.3918 (3.123x) 1.4616 → 0.5984 (-59.1%) 1536.1 → 512.1 (-1024.0)
BF16 1.00 1.4979 → 0.4736 (3.163x) 1.9240 → 0.8460 (-56.0%) 1536.1 → 512.1 (-1024.0)
FP16 0.10 1.0042 → 0.3277 (3.064x) 1.1451 → 0.4502 (-60.7%) 1536.1 → 512.1 (-1024.0)
FP16 0.25 1.0876 → 0.3480 (3.125x) 1.2226 → 0.4690 (-61.6%) 1536.1 → 512.1 (-1024.0)
FP16 0.50 1.2107 → 0.3923 (3.086x) 1.4443 → 0.5954 (-58.8%) 1536.1 → 512.1 (-1024.0)
FP16 1.00 1.4880 → 0.4766 (3.122x) 1.8980 → 0.8446 (-55.5%) 1536.1 → 512.1 (-1024.0)

Environment: NVIDIA H100 80GB HBM3, driver 570.195.03, PyTorch 2.7.1+cu128, Triton 3.3.1, 20 warmups / 100 measured iterations, seed 0. Base: 0b12d342; PR: fd6fde54. The focused correctness suite also passed: 53 tests.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

import argparse
import csv
import json
import shlex
import statistics
import subprocess
import sys
import time
from dataclasses import dataclass
Expand Down Expand Up @@ -89,6 +92,13 @@ def _sync(device: torch.device) -> None:


def _time_ms(fn, device: torch.device, *, warmup: int = 3, repeat: int = 10) -> tuple[Any, float]:
result, elapsed = _time_samples_ms(fn, device, warmup=warmup, repeat=repeat)
return result, statistics.median(elapsed)


def _time_samples_ms(
fn, device: torch.device, *, warmup: int = 3, repeat: int = 10
) -> tuple[Any, list[float]]:
result = None
for _ in range(max(0, warmup)):
result = fn()
Expand All @@ -112,7 +122,7 @@ def _time_ms(fn, device: torch.device, *, warmup: int = 3, repeat: int = 10) ->
elapsed.append((time.perf_counter() - start_time) * 1000.0)

_sync(device)
return result, statistics.median(elapsed)
return result, elapsed


def _peak_memory_gb(device: torch.device) -> float:
Expand All @@ -127,6 +137,164 @@ def _reset_peak(device: torch.device) -> None:
torch.cuda.reset_peak_memory_stats(device)


def _incremental_peak_bytes(fn, device: torch.device) -> int:
_reset_peak(device)
baseline = torch.cuda.memory_allocated(device)
result = fn()
_sync(device)
peak = torch.cuda.max_memory_allocated(device) - baseline
del result
return peak


def _backward_row(config: BenchmarkConfig) -> dict[str, Any]:
if config.device.type != "cuda":
raise RuntimeError("backward suite requires CUDA")

from rl_engine.kernels.ops.triton.loss.ratio_kl import TritonRatioKLOp

batch = make_synthetic_rl_kernel_batch(
num_prompts=config.num_prompts,
samples_per_prompt=config.samples_per_prompt,
prompt_len=config.prompt_len,
completion_len=config.completion_len,
vocab_size=config.vocab_size,
valid_density=config.mask_density,
dtype=config.dtype,
device=config.device,
seed=config.seed,
)
shape = (batch.batch_size, batch.completion_len, config.vocab_size)
torch.manual_seed(config.seed)
policy = torch.randn(shape, device=config.device, dtype=config.dtype)
ref = torch.randn_like(policy)
grad_ratio = torch.randn(*shape[:-1], 2, device=config.device)[:, :, 0]
grad_kl = torch.randn(*shape[:-1], 2, device=config.device)[:, :, 0]
op = TritonRatioKLOp()

def measure_isolated_backward():
isolated_policy = policy.detach().requires_grad_(True)
ratio, kl = op(
isolated_policy,
ref,
batch.token_ids,
batch.completion_mask,
batch.old_logps,
)

def isolated_backward():
isolated_policy.grad = None
torch.autograd.backward((ratio, kl), (grad_ratio, grad_kl), retain_graph=True)
return isolated_policy.grad

last_grad, samples = _time_samples_ms(
isolated_backward,
config.device,
warmup=config.warmup,
repeat=config.repeat,
)
isolated_policy.grad = None
del last_grad
peak = _incremental_peak_bytes(isolated_backward, config.device)
isolated_policy.grad = None
return samples, peak

isolated_samples, isolated_peak = measure_isolated_backward()

torch.cuda.empty_cache()

def forward_backward():
current_policy = policy.detach().requires_grad_(True)
current_ratio, current_kl = op(
current_policy,
ref,
batch.token_ids,
batch.completion_mask,
batch.old_logps,
)
torch.autograd.backward((current_ratio, current_kl), (grad_ratio, grad_kl))
return current_policy.grad

_, forward_backward_samples = _time_samples_ms(
forward_backward,
config.device,
warmup=config.warmup,
repeat=config.repeat,
)

direct_output = torch.version.hip is None and config.dtype in (
torch.float16,
torch.bfloat16,
)
return {
"shape": list(shape),
"dtype": str(config.dtype),
"mask_density": config.mask_density,
"valid_tokens": batch.benchmark_metadata()["valid_tokens"],
"isolated_backward_ms": isolated_samples,
"isolated_backward_median_ms": statistics.median(isolated_samples),
"forward_backward_ms": forward_backward_samples,
"forward_backward_median_ms": statistics.median(forward_backward_samples),
"incremental_peak_bytes": isolated_peak,
"expected_direct_output_bytes": (
policy.numel() * policy.element_size() if direct_output else 0
),
"expected_staging_saving_bytes": 4 * policy.numel() if direct_output else 0,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def _metadata_value(command: list[str]) -> str:
try:
return subprocess.check_output(command, text=True).strip()
except (subprocess.CalledProcessError, OSError):
return "unknown"


def _write_backward_results(
rows: list[dict[str, Any]], config: BenchmarkConfig, output: Path | None
) -> Path:
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
sha = _metadata_value(["git", "rev-parse", "HEAD"])
output = output or REPO_ROOT / ".cache/benchmarks/ratio_kl" / f"raw-{sha[:8]}-{stamp}.json"
output.parent.mkdir(parents=True, exist_ok=True)
driver = _metadata_value(["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"])
payload = {
"metadata": {
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
"git_sha": sha,
"gpu": torch.cuda.get_device_name(config.device),
"compute_capability": list(torch.cuda.get_device_capability(config.device)),
"driver": driver,
"torch": torch.__version__,
"triton": __import__("triton").__version__,
"backend": "TritonRatioKLOp",
"seed": config.seed,
"warmup": config.warmup,
"iterations": config.repeat,
"command": shlex.join([sys.executable, *sys.argv]),
},
"results": rows,
}
output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
lines = [
"# ratio_kl backward benchmark",
"",
f"Raw data: `{output.name}`",
"",
"| dtype | shape | density | isolated ms | forward+backward ms | peak MiB |",
"| --- | --- | ---: | ---: | ---: | ---: |",
]
lines.extend(
f"| {row['dtype']} | {row['shape']} | {row['mask_density']} | "
f"{row['isolated_backward_median_ms']:.4f} | "
f"{row['forward_backward_median_ms']:.4f} | "
f"{row['incremental_peak_bytes'] / 2**20:.1f} |"
for row in rows
)
output.with_suffix(".md").write_text("\n".join(lines) + "\n", encoding="utf-8")
return output


def _ratio_kl_row(config: BenchmarkConfig) -> dict[str, Any]:
candidate_name = "TritonRatioKLOp"

Expand Down Expand Up @@ -248,6 +416,11 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Fused ratio/KL RL-Kernel benchmark runner")
parser.add_argument("--case", default="ratio_kl", choices=["ratio_kl"])
parser.add_argument("--candidate", default="triton", choices=["triton"])
parser.add_argument(
"--backward-suite",
action="store_true",
help="Measure isolated backward, forward+backward, and incremental peak VRAM.",
)
parser.add_argument("--smoke", action="store_true", help="Run a small local-development shape")
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
parser.add_argument("--dtype", default="float16")
Expand Down Expand Up @@ -305,8 +478,12 @@ def main() -> None:
repeat=args.repeat,
)
try:
rows.append(_ratio_kl_row(config))
rows.append(
_backward_row(config) if args.backward_suite else _ratio_kl_row(config)
)
except torch.cuda.OutOfMemoryError as exc:
if args.backward_suite:
raise
rows.append(
{
"timestamp": datetime.now(timezone.utc).isoformat(),
Expand All @@ -333,7 +510,12 @@ def main() -> None:
}
)

_write_rows(rows, args.output)
if args.backward_suite:
output = _write_backward_results(rows, config, args.output)
print(output)
print(output.with_suffix(".md"))
else:
_write_rows(rows, args.output)


if __name__ == "__main__":
Expand Down
11 changes: 11 additions & 0 deletions docs/operators/ratio-kl.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ grad_policy_logits[v] = c * (1[v == action] - softmax_policy(v))
so the backward also avoids materializing any `[B, T, V]` probability tensor (only the
unavoidable `[B, T, V]` gradient output is written).

On NVIDIA CUDA, FP16/BF16 backward writes directly in the policy dtype, with explicit
`+0` for inactive rows. FP32 and ROCm retain the pre-zeroed FP32 staging path.

## Tensor Contract

| Argument | Shape | Dtype | Requirements |
Expand Down Expand Up @@ -83,8 +86,16 @@ The Triton op matches the native reference on `ratio` and `kl` (forward) and on
```bash
python benchmarks/benchmark_ratio_kl.py
python benchmarks/benchmark_ratio_kl.py --g-sizes 8 --completion-lens 512 --vocab-sizes 32768,131072
python benchmarks/benchmark_ratio_kl.py --backward-suite --smoke --dtype float16 \
--warmup 10 --repeat 50
```

The backward suite records isolated backward, forward+backward, and incremental peak VRAM
under `.cache/benchmarks/ratio_kl/`. Formal FP16/BF16 base/head validation uses an H100
with 20 warmups and 100 iterations. On an H100 PCIe at `[B,T,V]=[32,256,32768]`, the
direct-output backward saved exactly 1 GiB, ran 1.60–2.34× faster in isolation, and
improved forward+backward by 2.4–30.6% across FP16/BF16 at 10% and 90% mask density.

Indicative forward-only results (fp16, `B=16`, `T=512`):

| vocab | active tokens | forward speedup | peak VRAM (native → Triton) |
Expand Down
31 changes: 26 additions & 5 deletions rl_engine/kernels/ops/triton/loss/ratio_kl.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,15 @@ def _ratio_kl_bwd_kernel(
logz_ptr,
grad_ratio_ptr,
grad_kl_ptr,
grad_policy_ptr, # [N, V] fp32, pre-zeroed
grad_policy_ptr, # [N, V], pre-zeroed unless WRITE_INACTIVE_ZERO
V,
BLOCK_V: tl.constexpr,
WRITE_INACTIVE_ZERO: tl.constexpr,
):
row = tl.program_id(0)
row_off = row.to(tl.int64) * V
active = tl.load(mask_ptr + row) != 0
if active:
row_off = row.to(tl.int64) * V
a = tl.load(action_ptr + row)
ratio = tl.load(ratio_ptr + row)
d = tl.load(diff_ptr + row)
Expand All @@ -120,6 +121,10 @@ def _ratio_kl_bwd_kernel(
onehot = tl.where(cols == a, 1.0, 0.0)
grad = c * (onehot - soft)
tl.store(grad_policy_ptr + row_off + cols, grad, mask=cmask)
elif WRITE_INACTIVE_ZERO:
for start in range(0, V, BLOCK_V):
cols = start + tl.arange(0, BLOCK_V)
tl.store(grad_policy_ptr + row_off + cols, 0.0, mask=cols < V)


class _RatioKLFunction(torch.autograd.Function):
Expand Down Expand Up @@ -169,13 +174,29 @@ def backward(ctx, grad_ratio, grad_kl):
n_rows, V = pol.shape
gr = grad_ratio.contiguous().view(-1).to(torch.float32)
gk = grad_kl.contiguous().view(-1).to(torch.float32)
grad_pol = torch.zeros_like(pol, dtype=torch.float32)
direct_output = torch.version.hip is None and pol.dtype in (torch.float16, torch.bfloat16)
grad_pol = (
torch.empty_like(pol) if direct_output else torch.zeros_like(pol, dtype=torch.float32)
)

_ratio_kl_bwd_kernel[(n_rows,)](
pol, act, mask, ratio, diff, logz, gr, gk, grad_pol, V, BLOCK_V=ctx.block_v
pol,
act,
mask,
ratio,
diff,
logz,
gr,
gk,
grad_pol,
V,
BLOCK_V=ctx.block_v,
WRITE_INACTIVE_ZERO=direct_output,
)

grad_pol = grad_pol.view(ctx.policy_shape).to(ctx.policy_dtype)
grad_pol = grad_pol.view(ctx.policy_shape)
if not direct_output:
grad_pol = grad_pol.to(ctx.policy_dtype)
# policy_logits, ref_logits, action_ids, attention_mask, old_logps
return grad_pol, None, None, None, None

Expand Down
Loading
Loading