RADV lowers saturatedConvertEXT to FP8 as a NaN-preserving clamp per element (v_minimummaximum_f32) followed by v_cvt_pk_fp8_f32, because the hardware's FP16_OVFL saturation turns infinities into NaN instead of +-448. Run over all 2^32 f32 inputs on an RX 9070 XT, FP16_OVFL gives the clamp's result for every finite value and NaN; only +-Inf differ. The f32 values the native stages convert are never infinite (sums of FP8 products and binary16 biases, far below 3.4e38). Writing the conversion as "an infinity becomes NaN, then saturate" (FINITE in rr_exact.glsl) makes it the pattern nir_opt_algebraic turns into f2e4m3fn_satfn (vkd3d-proton's, Mesa >= 25.2, the same release that added the FP8 conversions): the hardware conversion with FP16_OVFL and no clamp. RR_SAT_CLAMP=1 builds the old form. 168 - 232 fewer instructions per network stage (enc0 960 -> 128 clamps). Every image byte-identical on the replay inputs (also robustness2). RX 9070 XT, 720p -> 1440p: frame 3.92 -> 3.79 ms, every network stage faster (dec0_1 607 -> 576 us, enc0 752 -> 727, enc1 262 -> 247). Signed-off-by: itchyfeetleech <[email protected]>
199 lines
11 KiB
GLSL
199 lines
11 KiB
GLSL
// Exact NVIDIA arithmetic helpers for the native Ray Reconstruction stages (engine/rr/*_m.comp, enc1_native.comp).
|
|
// Requires GL_EXT_shader_explicit_arithmetic_types, GL_EXT_float_e4m3 (RR_GFX11: GL_KHR_shader_subgroup_shuffle instead;
|
|
// FP8 values are then codes, rr_gfx11.glsl).
|
|
//
|
|
// RADV folds a register-only float(float16_t(x)) back to x, so every binary16 rounding whose result is used again goes
|
|
// through a conversion with an opaque zero (RR_ZERO16, set in main from something the compiler cannot evaluate, e.g.
|
|
// gl_NumWorkGroups.z - 1) OR-ed into its bits: one instruction more than the plain conversion.
|
|
uint16_t RR_ZERO16;
|
|
#if defined(RR_RELAXED)
|
|
// Relaxed rounding (build_native_model.sh with D4R_RR_RELAXED=1): the accumulators stay f32 between 32-deep steps and
|
|
// are converted to FP8 in one rounding instead of NVIDIA's two (binary16, then FP8). About 1% of the FP8 codes of a
|
|
// layer then differ by one step, so a stage no longer reproduces the reference bit for bit; it saves the conversions
|
|
// that are a quarter of a stage's instructions.
|
|
float rnd16(float x) { return x; }
|
|
#elif defined(RR_EXACT_CONVERT)
|
|
// the module's conversions are decorated NoContraction (engine/rr/spv_exact_convert.py): the driver keeps them
|
|
float rnd16(float x) { return float(float16_t(x)); }
|
|
#else
|
|
float rnd16(float x) { return float(uint16BitsToFloat16(float16BitsToUint16(float16_t(x)) | RR_ZERO16)); }
|
|
#endif
|
|
// x rounded to binary16 (nearest even), as f32 / as f16, through a conversion the compiler keeps (see rnd16)
|
|
float r16(float x) { return rnd16(x); }
|
|
#if (defined(RR_RELAXED) && RR_RELAXED >= 2) || defined(RR_EXACT_CONVERT)
|
|
float16_t h16(float x) { return float16_t(x); }
|
|
#else
|
|
float16_t h16(float x) { return uint16BitsToFloat16(float16BitsToUint16(float16_t(x)) | RR_ZERO16); }
|
|
#endif
|
|
// NVIDIA's k order within a 32-block: slot j holds natural channel inter(j); uninter is the inverse (and also the
|
|
// arena's channel order: arena slot uninter(k) holds natural channel k)
|
|
uint inter(uint j) { return (j & ~31u) | (((j & 31u) >> 2u) << 1u) | (j & 1u) | (((j >> 1u) & 1u) << 4u); }
|
|
uint uninter(uint c) { return (c & ~31u) | (((c & 15u) >> 1u) << 2u) | (c & 1u) | (((c >> 4u) & 1u) << 1u); }
|
|
int mirror(int v, int n) { v = abs(v); return min(v, 2 * n - 2 - v); }
|
|
|
|
// FINITE(v): v, but an infinity (which these f32 values never are: sums of FP8 products and binary16 biases stay far
|
|
// below 3.4e38) becomes NaN. Written so, a saturating FP8 conversion is RADV's f2e4m3fn_satfn (nir_opt_algebraic's
|
|
// vkd3d-proton pattern): the hardware conversion with FP16_OVFL, which saturates every finite value the same way,
|
|
// instead of a NaN-preserving clamp per element and then the conversion. RR_SAT_CLAMP=1 keeps the clamp, and so does
|
|
// RR_MIX, whose binary16 accumulators can overflow.
|
|
#if defined(RR_SAT_CLAMP) || defined(RR_MIX)
|
|
#define FINITE(v) (v)
|
|
#define FINITE2(v) (v)
|
|
#else
|
|
#define FINITE(v) (isinf(v) ? uintBitsToFloat(0x7fc00000u) : (v))
|
|
#define FINITE2(v) vec2(FINITE((v).x), FINITE((v).y))
|
|
#endif
|
|
|
|
#ifdef RR_GFX11
|
|
#include "rr_gfx11.glsl"
|
|
#define F8 uint
|
|
uint q8h(float16_t x) { return g11_code(x); }
|
|
uint q8s(float x) { return g11_codef(x); }
|
|
uint code(uint c) { return c; }
|
|
float16_t e4h(uint c) { return g11_e4h(c); }
|
|
// q8(act(h)) from the packed table (enc1_pack.act_table; the weight buffer's W8U view at W_ACT)
|
|
#define ACTQ(h) uint(w8u.w[W_ACT + bh(h)])
|
|
// an accumulator's activation codes (accumulator layout)
|
|
#define ACTQ8C(dst, h) { (dst) = uvec2(0u); [[unroll]] for (int i_ = 0; i_ < 8; ++i_) (dst)[i_ >> 2] |= ACTQ(float16_t((h)[i_])) << (8u * uint(i_ & 3)); }
|
|
#else
|
|
#define F8 floate4m3_t
|
|
float16_t e4h(floate4m3_t v) { return float16_t(v); }
|
|
// cvt.rn.satfinite.e4m3 (NaN -> 0x7f)
|
|
floate4m3_t q8(float16_t h)
|
|
{
|
|
return isnan(h) ? uintBitsToFloate4m3EXT(uint8_t(0x7fu)) : floate4m3_t(clamp(h, float16_t(-448.0), float16_t(448.0)));
|
|
}
|
|
// the same from f32 / f16 values in one saturating hardware conversion (satfinite; for an f32 value that is a binary16
|
|
// number the result equals the f16 conversion's)
|
|
floate4m3_t q8s(float x) { floate4m3_t r; saturatedConvertEXT(r, FINITE(x)); return r; }
|
|
floate4m3_t q8h(float16_t x) { floate4m3_t r; saturatedConvertEXT(r, x); return r; }
|
|
|
|
uint code(floate4m3_t v) { return uint(floate4m3BitsToUintEXT(v)); }
|
|
#endif
|
|
float16_t hb(uint b) { return uint16BitsToFloat16(uint16_t(b)); }
|
|
uint bh(float16_t h) { return uint(float16BitsToUint16(h)); }
|
|
|
|
// m * x compared with 1, exactly (the fma recovers the product's rounding error): -1, 0 or 1
|
|
int cmp1(float m, float x)
|
|
{
|
|
precise float t = m * x;
|
|
precise float e = fma(m, x, -t);
|
|
return t > 1.0 ? 1 : (t < 1.0 ? -1 : (e > 0.0 ? 1 : (e < 0.0 ? -1 : 0)));
|
|
}
|
|
// 1 / x of the filters' softmax and gate, rounded to binary16. The reference is the GPU's own reciprocal instruction
|
|
// (rcp.approx; ZLUDA lowers it to llvm.amdgcn.rcp.f32, which is what 1.0 / x compiles to here), not the correctly
|
|
// rounded quotient: correcting it left ~0.01% of the filtered surfaces one ulp off (capDT, 2026-10-12).
|
|
float16_t rcp16(float x) { return h16(1.0 / x); }
|
|
// binary16 rounding of 1 / sqrt(s) (s > 0)
|
|
float16_t rsq16(float16_t s16)
|
|
{
|
|
const float s = float(s16);
|
|
float16_t q = h16(inversesqrt(s));
|
|
#if defined(RR_RELAXED) && RR_RELAXED >= 2
|
|
return q;
|
|
#endif
|
|
for (int i = 0; i < 2; ++i) {
|
|
const uint b = bh(q);
|
|
const float lo = (float(q) + float(hb(b - 1u))) * 0.5, hi = (float(q) + float(hb(b + 1u))) * 0.5;
|
|
const int cl = cmp1(lo * lo, s), ch = cmp1(hi * hi, s);
|
|
if (cl > 0 || (cl == 0 && (b & 1u) != 0u)) q = hb(b - 1u);
|
|
else if (ch < 0 || (ch == 0 && (b & 1u) != 0u)) q = hb(b + 1u);
|
|
}
|
|
return q;
|
|
}
|
|
// fma of binary16 operands with one binary16 rounding, exactly. The product is exact in f32, so an f32 fma rounds the
|
|
// exact result once; converting that to binary16 rounds twice, which differs only when the f32 value lands exactly
|
|
// on a binary16 midpoint: then the sum's rounding error (TwoSum, the rare branch) decides the direction.
|
|
float16_t fma16(float16_t a, float16_t b, float16_t c)
|
|
{
|
|
const float fa = float(a), fb = float(b), fc = float(c);
|
|
precise float s = fma(fa, fb, fc);
|
|
#if defined(RR_RELAXED) && RR_RELAXED >= 2
|
|
return float16_t(s);
|
|
#endif
|
|
const uint u = floatBitsToUint(s);
|
|
if ((u & 0x1fffu) == 0x1000u && abs(s) >= 6.103515625e-05) {
|
|
precise float p = fa * fb;
|
|
precise float bb = s - p;
|
|
precise float err = (p - (s - bb)) + (fc - bb);
|
|
if (err != 0.0) return h16(uintBitsToFloat(err * s > 0.0 ? u + 1u : u - 1u));
|
|
}
|
|
return h16(s);
|
|
}
|
|
// the same in one f32 fma, rounded once more to binary16: mid is set when the f32 result lies on a binary16 midpoint,
|
|
// where that double rounding can differ (the caller then redoes the computation with fma16)
|
|
float16_t fma16q(float16_t a, float16_t b, float16_t c, inout bool mid)
|
|
{
|
|
precise float s = fma(float(a), float(b), float(c));
|
|
#if defined(RR_RELAXED) && RR_RELAXED >= 2
|
|
return float16_t(s);
|
|
#endif
|
|
mid = mid || (floatBitsToUint(s) & 0x1fffu) == 0x1000u;
|
|
#ifdef RR_EXACT_CONVERT
|
|
return float16_t(s);
|
|
#else
|
|
return hb(bh(float16_t(s)) | uint(RR_ZERO16));
|
|
#endif
|
|
}
|
|
#ifndef RR_GFX11
|
|
// The saturating FP8 conversion of a whole accumulator, two elements per conversion instruction
|
|
#define Q8PAIRS(dst, src) [[unroll]] for (int i_ = 0; i_ < 8; i_ += 2) { fe4m3vec2 t_; saturatedConvertEXT(t_, FINITE2(vec2((src)[i_], (src)[i_ + 1]))); (dst)[i_] = t_.x; (dst)[i_ + 1] = t_.y; }
|
|
// q8(act(h)) from the packed table (enc1_pack.act_table; the weight buffer's W8U view at W_ACT)
|
|
#define ACTQ(h) uintBitsToFloate4m3EXT(w8u.w[W_ACT + bh(h)])
|
|
// the activation of a whole accumulator into FP8. RR_NATIVE (D4R_RR_RELAXED=3): in f32, two elements per
|
|
// instruction, straight into FP8 - no binary16 in between and no table (not the reference's roundings)
|
|
#ifdef RR_NATIVE
|
|
#define ACTQ8(dst, h) [[unroll]] for (int i_ = 0; i_ < 8; i_ += 2) { const vec2 x_ = vec2((h)[i_], (h)[i_ + 1]); const vec2 y_ = clamp(x_, vec2(-2.0), vec2(2.0)); const vec2 g_ = fma(y_, fma(-abs(y_), vec2(0.0810546875), vec2(0.412109375)), vec2(0.5)); fe4m3vec2 t_; saturatedConvertEXT(t_, FINITE2(x_ * g_)); (dst)[i_] = t_.x; (dst)[i_ + 1] = t_.y; }
|
|
#else
|
|
#define ACTQ8(dst, h) [[unroll]] for (int i_ = 0; i_ < 8; ++i_) (dst)[i_] = ACTQ(float16_t((h)[i_]));
|
|
#endif
|
|
#endif
|
|
float16_t act(float16_t x)
|
|
{
|
|
precise float16_t y = clamp(x, float16_t(-2.0), float16_t(2.0));
|
|
precise float16_t a = float16_t(0.0810546875) * abs(y);
|
|
precise float16_t b = float16_t(0.412109375) - a;
|
|
precise float16_t c = y * b;
|
|
precise float16_t g = float16_t(0.5) + c;
|
|
precise float16_t r = x * g;
|
|
return r;
|
|
}
|
|
float16_t sig(float16_t v) { return rcp16(exp2(float(v) * -1.4426950216293335) + 1.0); }
|
|
// the cubic-exponent softmax numerator (enc1_ref.softmax_cubic): a cubic in the clamped scaled score whose binary16
|
|
// bits, shifted into the exponent field, are the exponential
|
|
// the same in plain f32 fmas (double rounding); bad is set when an f32 result lies exactly on a binary16 midpoint,
|
|
// the only case where that differs: the caller then recomputes with cubic_e (subgroupAny, rare)
|
|
float16_t cubic_e_fast(float16_t x, inout bool bad)
|
|
{
|
|
precise float16_t m = clamp(x * float16_t(0.017181396484375), float16_t(-0.55615234375), float16_t(0.55615234375));
|
|
const float fm = float(m);
|
|
precise float t = fma(fm, -fm, 0.92724609375);
|
|
const float tr = rnd16(t);
|
|
precise float u = fma(fm, tr, 1.375);
|
|
// t - c and u - 1.375 are exact (Sterbenz), so one more fma gives the rounding error's sign: only an inexact
|
|
// midpoint is a double rounding
|
|
precise float et = fma(fm, fm, t - 0.92724609375), eu = fma(fm, -tr, u - 1.375);
|
|
bad = bad || ((floatBitsToUint(t) & 0x1fffu) == 0x1000u && et != 0.0) || ((floatBitsToUint(u) & 0x1fffu) == 0x1000u && eu != 0.0);
|
|
return hb((bh(float16_t(u)) << 5u) & 0x7FE0u);
|
|
}
|
|
float16_t cubic_e(float16_t x)
|
|
{
|
|
precise float16_t m = clamp(x * float16_t(0.017181396484375), float16_t(-0.55615234375), float16_t(0.55615234375));
|
|
const float16_t t = fma16(m, -m, float16_t(0.92724609375));
|
|
const float16_t u = fma16(m, t, float16_t(1.375));
|
|
return hb((bh(u) << 5u) & 0x7FE0u);
|
|
}
|
|
// FP8 operands from a shared byte image (word offset and stride): an A row-major, a B column-major; and an accumulator
|
|
// quantized and stored as the transposed A row-major / as a B column-major (the same bytes)
|
|
#ifdef RR_GFX11
|
|
#define LDS8A(m, ARR, off, stride) LD8A(m, ARR, off, stride)
|
|
#define LDS8B(m, ARR, off, stride) LD8B(m, ARR, off, stride)
|
|
#define Q8STA(src, ARR, off, stride) { F8V c_; G11_CODES(c_, src) G11_ST(c_, ARR, off, stride) }
|
|
#define Q8STB(src, ARR, off, stride) Q8STA(src, ARR, off, stride)
|
|
#else
|
|
#define LDS8A(m, ARR, off, stride) coopMatLoad(m, ARR, off, stride, gl_CooperativeMatrixLayoutRowMajor)
|
|
#define LDS8B(m, ARR, off, stride) coopMatLoad(m, ARR, off, stride, gl_CooperativeMatrixLayoutColumnMajor)
|
|
#define Q8STA(src, ARR, off, stride) { A8 q; Q8(q, src) coopMatStore(q, ARR, off, stride, gl_CooperativeMatrixLayoutRowMajor); }
|
|
#define Q8STB(src, ARR, off, stride) { B8 q; Q8(q, src) coopMatStore(q, ARR, off, stride, gl_CooperativeMatrixLayoutColumnMajor); }
|
|
#endif
|