Publish-ready main: measured results, contribution setup, and honest scope - #1
Conversation
Both were easy to overstate, so both are now measured or narrowed. Measured forge.patch at inference on an idle A100 with real Qwen2.5-0.5B weights, patching one kernel at a time. It is correct — greedy generation is token-identical to eager and logits agree to 1.8e-02 relative in bf16 — and it is 15% slower, at prefill and during generation alike. RMSNorm (0.91x, 49 modules) and SwiGLU (0.90x, 24 modules) account for nearly all of it; embedding, rope and fused_linear_ce are roughly neutral. That is a shape effect rather than a defect: those wins are recorded at hidden=4096 / intermediate=11008 over 8192 rows, and a 0.5B model at batch 1 sits well below the size where a fused kernel amortises its launch and autotune cost. The failure mode matters though — patch() applies kernels unconditionally, so someone patching a small model gets a slowdown while believing the opposite. Filed as a shape-based dispatch guard. Also narrowed the multi-GPU wording. What FSDP2 verification establishes is that patched forwards keep working once weights become sharded DTensors, which is non-trivial — an earlier LoRA MLP path crashed exactly there — but there is no custom collective and no comm/compute overlap. The 2-GPU results show equivalence to the single-GPU reference, not a speedup from scaling. Adds CODEOWNERS and documents the branch policy: work lands via PR into dev, main is release-only, and both are protected. Co-authored-by: Cursor <cursoragent@cursor.com>
`bench_all.py --list` ends with the legend line "! = script missing from working tree", and the check grepped for a leading "!", so it flagged the legend and failed on a healthy tree. Require a non-'=' first character. Verified both directions: clean on the current tree, and it still catches a real missing script when one is moved away. Co-authored-by: Cursor <cursoragent@cursor.com>
forge.patch applied every kernel to every module, so patching a small model produced a 15% regression while reporting success. Measuring across shapes showed the deciding variable is activation elements per call, not training vs inference as the README previously claimed: the same RMSNorm that loses 9% at 128 rows wins 6% at 4096, because below ~1024 rows a 0.5B model is launch-bound and eager takes as long for 128 tokens as for 1024. Shape-sensitive kernels now check each call against MIN_FUSED_ELEMENTS and defer to the original forward below it. The threshold tracked the measured crossover across a 4.5x change in width. Worst case goes 0.85x -> 0.98x, with training on Qwen2.5-0.5B unchanged at 1.20x and 36% less memory. Also here: - mode="infer" skips the loss and LoRA kernels, which cannot help without a backward pass, and records why in model._forge_skipped. - RoPE declines head_dim values it cannot handle rather than raising a bare AssertionError from inside attention. 80 and 96 both occur in published models. Compatibility checks stay in force when min_elements=0, since a performance knob must not switch off a correctness one. - Fix stale positional args in forge/tests/test_lora_mlp.py that put a bool in a tensor slot: 0/8 -> 8/8. Same bug as the one fixed in bench_memory.py. - bench_patch_bisect.py measures one kernel at a time on a real model, which is how the threshold was calibrated and how to re-derive it elsewhere. - Correct the README section that overgeneralised from a single shape. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Added 7e3fa10, which corrects a claim made earlier in this PR. The README section titled "This is a training library" said
Inference at batch 4 is 1.12× faster. Below ~1024 rows a 0.5B model is launch-bound rather than compute-bound — eager needs the same 32 ms for 128 tokens as for 1024 — so 73 extra kernel launches can only cost. Training also gains 36% peak memory (21.9 GB → 14.1 GB), nearly all of it from fused linear cross-entropy: Qwen2.5-0.5B pairs an 896-wide hidden state with a 151936-token vocabulary, so not materialising the logits saves 6.2 GB on its own.
Also in the commit:
Verification after the change, all on an idle A100: |
What this changes
Brings
devintomainas the first publish-ready state: measured benchmark results for the two kernels that had none, a tidied tree, a contribution setup, and two scope claims narrowed to what is actually measured.Numbers
Fused linear cross-entropy, the kernel whose harness had never been executed, on an idle A100-80GB, bf16,
h=4096, v=128256, full fwd+bwd:Two causes, both in the chunk loop: a per-chunk
(V, H)fp32 temporary that made chunking buy no memory at all, and aBT/(V/H)chunk rule that re-read the weight 32 times per pass. Result files are committed underkernels/cross_entropy/benchmarks/results/.forge.patchat inference, real Qwen2.5-0.5B weights, one kernel at a time — correct but slower, so the scope is now stated plainly in the README:nvidia-smi), device pinned withCUDA_VISIBLE_DEVICESCorrectness
uv run ruff check .— green, with a narrow rule set configured rather than reformatting ~700 inherited findingsBoth documented pytest commands were broken and now work.
pytest tests/could not be collected at all (9 errors) becausetests/layernorm/is a package, so pytest puttests/onsys.pathinstead of the repo root. Fixing that surfaced 4 real failures in the LayerNorm dW comparison, which turned out to be tolerance rather than gradient: against a float64 reference the kernel is closer to the truth than eager (2.16e-05 vs 2.92e-05), so a flatatol=1e-5was failing the more accurate result.Anything a reviewer should know
hackathon-archive.forgepackage is not installable today — the built wheel contains zero kernels andimport forgefails outside a checkout. Filed as a blocker; not addressed here.Made with Cursor