Uh oh!
There was an error while loading. Please reload this page.
prototype: port teeny's portable half/bfloat16 to C++11 (core/half.h) - #92
Open
balbasty wants to merge 4 commits into
Open
prototype: port teeny's portable half/bfloat16 to C++11 (core/half.h)#92balbasty wants to merge 4 commits into
balbasty wants to merge 4 commits into
Conversation
Prototype only -- nothing else in the tree includes this header, and it is
not wired into `make test`. It answers "could we take teeny's half.h, and
would it work on C++11" with a compiling, exhaustively-tested artefact; the
separate question of whether the dispatch layer can afford two more dtypes
is answered in the PR body, and the answer there is "not today".
core/half.h, ported from balbasty/teeny include/teeny/half.h (MIT, same
copyright holder and same licence as this repo, so no new obligation beyond
the provenance notice the file carries). Under nvcc `ff::half`/`ff::bfloat16`
ARE `__half`/`__nv_bfloat16`; on a host compiler they are portable software
structs with the same 16-bit layout. `compute_type<T>` makes both accumulate
in float.
C++11 changes vs teeny, all mechanical:
- <cuda/std/*> -> <type_traits> + the <cstdint> cuda_switch.h provides.
libcu++ (CCCL) enforces a C++17 floor; this repo is C++11 with no CCCL.
- cs::enable_if_t (C++14) -> typename std::enable_if<...>::type
- _TNY_API -> CUHOSTDEV, TNY_* -> FF_*
Two changes that are NOT mechanical, both found by compiling against the
real kernels rather than by reading:
- Mixed-type operators (FF_HALF_MIXED). teeny's implicit converting
constructor plus implicit operator float() make every `reduce_t OP
scalar_t` expression AMBIGUOUS -- 416 errors across 10 kernel sites in
pushpull alone. Spelling out the heterogeneous overloads (as PyTorch's
c10::Half does) resolves all of them to the wider type with no kernel
edited. An `explicit` constructor instead would fix these and break
every `scalar_t x = <reduce_t expr>` site, so it is not the answer.
- The NSDMI on `bits` is dropped. teeny's `uint16_t bits{}` makes the
defaulted default constructor non-trivial, so `__shared__ half tile[N]`
would not compile under nvcc and `half x;` silently zero-stores. It buys
nothing: the user-provided converting constructor already costs
aggregate-ness in C++11. Trivially copyable is preserved and asserted.
tests/core/test_half.cpp -- exhaustive, not spot-checked: all 2^16 half and
bf16 patterns decoded, and all 2^32 float patterns encoded, bit-compared
against the compiler's own _Float16/__bf16 (clang 15+/gcc 12+ provide both
in C++11 mode on x86-64). That covers every subnormal, overflow boundary and
round-to-nearest-even tie by construction. NaN is exempted: the software
conversions canonicalise the payload where hardware preserves it. Named
cases follow for readable failures. Run with `make test-half` (~6 min);
`make test-half STRIDE=999999` subsamples.
tests/impl-cuda/compile_probe_half{,_portable}.cu -- nvcc compile probes for
both paths, picked up by the existing `make test-impl-cuda`. The portable one
forces FF_PORTABLE_HALF so the software conversions are compiled as DEVICE
code, which is what a future host-vs-hardware comparison on a real GPU needs.
Verified: clang++ 18 and g++ 13, -std=c++11 and -std=c++14, -Wall -Wextra
-Wpedantic clean, exhaustive test passing. The nvcc legs are unverified
locally (no CUDA toolkit in this environment) and must go green in CI.
`make test` is untouched -- the only edit to an existing file is the new
test-half target -- so the 59,886-check/13-suite gate cannot move.The lint job checks only the lines a PR touches, and every line here is new, so all four files are in scope. Reformatted to .clang-format, with the two column-aligned backslash macro tables (FF_HALF_MIXED, FF_HALF_TYPE) fenced in clang-format off/on -- clang-format re-wraps line continuations and destroys the alignment that makes those tables readable. Re-verified after reformatting: clang++ 18 and g++ 13, -std=c++11 and -std=c++14, -Wall -Wextra -Wpedantic clean, test passing.
balbasty
commented
Aug 20, 2026
CollaboratorAuthor
ok |
Conflict in the top-level Makefile: main added a test-atomics target to the same .PHONY line this branch adds test-half to. Both are kept.
Four conventions landed after this branch was cut; the new files predate all of them. * #pragma once on line 1, and the whole-file `#ifndef FF_HALF` guard removed (#145). The file had acquired BOTH, which is the one combination the convention rules out -- core/dlpack.h is the sole guard exception and it is vendored, so its upstream macro can collapse with a system DLPack header's. * FF_NAMESPACE_BEGIN/END(FF_NS), not (FF): #91 renamed the root-namespace macro, because a bare two-letter all-caps name in an installed header takes that identifier away from every downstream translation unit. * FF_CUHOSTDEV, not the unprefixed CUHOSTDEV, for the same reason (#91). * <fastfields/...> for the public interface, here and in the three test and probe files that include it (#146). Applied with tools/normalise-header-guards.py, normalise-include-delimiters.py and rename-macros.py; all three now pass --check, as does check-cuda-launches.py. The continuation backslashes in the two clang-format-off macro blocks are re-aligned to the new column so the blocks stay readable. No behaviour change: FF_CUHOSTDEV and CUHOSTDEV expand identically, and the namespace is `ff` either way.
balbasty
marked this pull request as ready for review
August 20, 2026 22:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Answers the question "should we steal teeny's portable half/bf16, and would it work on C++11?"
Short version: yes — the header ports cleanly and is done here, verified on clang++, g++ and nvcc. It is a header and nothing more. Nothing includes it, no dtype dispatch gained a 16-bit arm, and the two things that actually block half/bf16 support — the CUDA atomics of #100 and the instantiation budget — are untouched and still open.
Brought up to current
main(247714e, through #155) by merging rather than rebasing, so the push stays a fast-forward and no one's checkout of this branch is rewritten. One conflict, in the top-levelMakefile:mainadded atest-atomicstarget to the same.PHONYline this branch addstest-halfto. Both are kept, and nothing else in the branch collided.Scope — what this is not
This is worth being blunt about, because the analysis below is long enough to read like a plan:
case kDLFloat/16 arm was added anywhere. The dispatch layer still throws on a 16-bit tensor, and the tentests/lib-cpu/test_*.cppsuites that assert float16 must throw still pass unchanged.anyAtomicAdd(half*, half)is still a hard compile error in this branch, deliberately.What it is: a self-contained header, an exhaustive conversion test, two nvcc compile probes, and one new
maketarget.make testis untouched.1. C++11: yes, definitively
Verified under clang++ and g++,
-std=c++11and-std=c++14,-Wall -Wextra -Wpedanticclean, and under nvcc viamake test-impl-cuda. Every blocker in the original reading was real and shallow:<cuda/std/type_traits>/<cuda/std/cstdint>→<type_traits>plus the<cstdint>thatcore/cuda_switch.halready provides. libcu++ (CCCL) enforces a C++17 floor; this repo is C++11 (C++14 for nvcc) and has no CCCL dependency.cs::enable_if_t<...>→typename std::enable_if<...>::type. Trivial._TNY_API→FF_CUHOSTDEV.memcpy, which those compilers fold to the same register move. Free correctness, taken.One correction to the original reading.
uint16_t bits{}is C++11-legal, but the NSDMI is not what costs aggregate-ness — the user-provided templated converting constructor already does that. What the NSDMI actually costs is a trivial default constructor, which means__shared__ half tile[N]will not compile under nvcc andhalf x;silently zero-stores. Dropped it; trivial default construction and trivial copyability are both restored and bothstatic_asserted.compile_probe_half_portable.cucompiles a__shared__ ff::half tile[32]under nvcc precisely so this cannot regress.2. What actually bites (found by compiling, not by reading)
The header ports mechanically. Pointing it at the real kernels does not. These were found while prototyping a half arm into the dispatch — that arm is not part of this PR, but what it turned up is the reason this header alone is not "half support":
reduce_t OP scalar_tis ambiguous. The implicit converting constructor makesoperator*(half, half)viable and the implicitoperator float()makes the built-in viable; neither wins. Fixed entirely inside the header with heterogeneous overloads (FF_HALF_MIXED), as PyTorch'sc10::Halfdoes — no kernel edited. Making the constructorexplicitinstead trades this class of error for a different one (no viable conversion from double to halfat everyscalar_t x = <literal>), so it is not the answer.bounds.h.cgetcasts toval_tand then returnsscalar_t, discarding the widening the call site asked for. Harmless for the current dtypes (float→double→floatis exact), but withscalar_t = halfthe value round-trips through 16 bits. Not fixed here — it touches shared bound helpers — but it is a real bug on its own merits.ff::is_floating_pointis false on the host (it is only specialised under__CUDACC__), which routes half into_mod's integer specialisation and itsx % d— a hard compile error.std::numeric_limitsis unspecialised, somax(),lowest()andinfinity()all silently return0.0with no diagnostic.The last three are silent-wrong-answer risks the moment half appears anywhere, and each is small and independently fixable.
3. Verification
make test-half— exhaustive. All 2^16 half and all 2^16 bf16 patterns decoded, and all 2^32 float patterns encoded in each direction, bit-compared against the compiler's own_Float16/__bf16. Every subnormal, every overflow boundary and every round-to-nearest-even tie is covered by construction rather than by a hand-picked list. NaN is exempted (software canonicalises the payload; hardware preserves it).make test-half STRIDE=Nsubsamples for a smoke run. Measured on the merged tree: 8.6 billion comparisons, clean under clang++-std=c++11(415 s) and g++-std=c++14(380 s).The oracle earned its keep: one named expectation in the test was wrong and it caught it —
FLT_MAXrounds up to bf16+inf, and bf16's largest finite is0x7f7f. The header was right.compile_probe_half.cu(native path — the__half/__nv_bfloat16aliases and templated device kernels) andcompile_probe_half_portable.cu(the software structs compiled as device code underFF_PORTABLE_HALF, including the__shared__array). Both are picked up automatically by the existingmake test-impl-cudaglob.tools/test-baseline.sh --legs default,libon this branch, diffed row for row againsttools/test-baseline.expected: 15 rows, 0 differ — 13 suites / 59,886 checks / 0 failures on thedefaultleg, plus the 2 hub suites (14 checks). All fivetest-cpulegs andtest-hubare green in CI besides. Nothing in this PR is reachable from any of them:core/half.his included by exactly two files, both new, neither built bymake test.(
--checkitself refuses a two-leg run — it compares whole reports and the recording covers six legs — so the fifteen rows were diffed against the recording directly.)4. Conventions
Brought up to what the tree now requires, which is the only substantive change since the last review:
#pragma onceon line 1, with the whole-file#ifndef FF_HALFguard removed (refactor: #pragma once in every header #145). The file had acquired both, which is the one combination the convention rules out.FF_NAMESPACE_BEGIN(FF_NS)/FF_NAMESPACE_END(FF_NS)—FFwas renamedFF_NSin refactor: prefix every remaining public macro with FF_ #91.FF_CUHOSTDEV, not the old unprefixedCUHOSTDEV(refactor: prefix every remaining public macro with FF_ #91).<fastfields/…>for the public interface, in the header and in all three test/probe files (refactor: spell the public interface <fastfields/...> #146).normalise-header-guards.py,normalise-include-delimiters.py,rename-macros.pyandcheck-cuda-launches.pyall pass--check.5. Licence and provenance
balbasty/teenyis MIT, © 2023-2025 Yaël Balbastre; this repository is MIT, © 2025 Yaël Balbastre. Same licence, same holder — no new obligation. The header carries a provenance block naming teeny, the licence, and every deviation made for fastfields. NoLICENSEchange is needed.6. Recommendation
Take the header; do not read it as a step towards half support that is now underway. It is cheap to carry, it retires the "would it even work on C++11" question with evidence, and it is inert. The work that would make half real — #100's atomics and alignment precondition, the instantiation budget, a GPU in CI, and a deliberate rewrite of the ten float16-must-throw cases — is all still ahead and none of it is started.
lint (clang-format, changed lines)is green on this branch. Thelint (source conventions)job #156 has since added did not exist when this run started, so it has not run here; the three checkers it invokes were run against this tree by hand and pass (section 4).