mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* codegen: auto-detect runtime-built f16 tables; fail on stale asserts The f16->f32 table's address had to be asserted from outside (-f16-table): ggml-style engines build the table at runtime, so the static byte-check cannot see it. That external magic number proved a liability twice over — the address moves with any layout shift (source changes, even build-cache state), a stale value silently disables every table-keyed rewrite (the llama-wasm v0.2.1 release shipped ~35% slower decode this way), and the existing staleness warning never ran for multi-package modules because that layout returns before the check. Derive the address from the module's own initialization code instead: an init loop that streams constant-strided stores from a constant base and provably covers the table's whole 256KiB range. Detection is structural over the lowered SSA — a loop-carried pointer phi with constant start and advance, a counter phi with a computable trip count, stores through the pointer — so it does not depend on how the compiler vectorized the conversion arithmetic inside the loop. The scan runs lazily on the first gather-site query. -f16-table remains as an explicit override, now cross-checked: a set address that contradicts a module-verified base FAILS the build, naming the address to adopt; the no-evidence case keeps the old warning, which now also runs on the multi-package path. Verified against the llama engine module: flagless transpile auto-detects the table and produces a byte-identical bundle to the correctly-asserted build (121 FCVT conversions intact); a stale assert aborts with the actionable error. Co-Authored-By: Claude Fable 5 <[email protected]> * codegen: drop the f16-table address input; keep only a boolean opt-out With auto-detection proven equivalent (flagless transpile of the engine module is byte-identical to the previously asserted build), an address-carrying override has no remaining legitimate use — it only reintroduces the stale-value hazard. -f16-table is gone; -no-f16-table disables the table-keyed rewrites outright. The end-of-translation diagnostic is now unconditional: gather sites whose base verified neither statically nor through init-loop coverage are reported, so a detection miss after a module change is loud in every pipeline log. Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]>
180 lines
5.0 KiB
Go
180 lines
5.0 KiB
Go
package codegen
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
)
|
|
|
|
// The f16 load-convert selection (see the scalarizer's gather rewrite)
|
|
// replaces reads of a module-resident f16->f32 lookup table with the
|
|
// FCVTL instruction. That is only sound when the table IS the IEEE
|
|
// conversion: the transpiler verifies the module's initial memory
|
|
// image byte-for-byte before enabling the rewrite. No pattern of
|
|
// table CONTENT is assumed — a module with any other table keeps the
|
|
// literal loads.
|
|
|
|
// f16BitsToF32Bits is the IEEE binary16 -> binary32 conversion,
|
|
// bit-exact including subnormals, infinities and NaN payloads
|
|
// (payload shifts left by 13; the quiet bit rides along). This is
|
|
// what FCVTL computes per lane and what such a table holds.
|
|
func f16BitsToF32Bits(h uint16) uint32 {
|
|
sign := uint32(h>>15) << 31
|
|
exp := uint32(h>>10) & 0x1F
|
|
man := uint32(h) & 0x3FF
|
|
switch exp {
|
|
case 0:
|
|
if man == 0 {
|
|
return sign // ±0
|
|
}
|
|
// Subnormal: value = man * 2^-24. Normalize into a binary32
|
|
// exponent.
|
|
e := uint32(113)
|
|
for man&0x400 == 0 {
|
|
man <<= 1
|
|
e--
|
|
}
|
|
return sign | e<<23 | (man&0x3FF)<<13
|
|
case 0x1F:
|
|
return sign | 0xFF<<23 | man<<13 // ±inf / NaN
|
|
}
|
|
return sign | (exp+112)<<23 | man<<13
|
|
}
|
|
|
|
// hasIEEEF16TableAt reports whether the module's data segments
|
|
// (active, or passive with a recovered start-section placement)
|
|
// fully populate [base, base+65536*4) with table[i] == the IEEE
|
|
// conversion of i. Bytes not covered by any active segment default to
|
|
// zero in the initial image; the table has almost no zero entries, so
|
|
// coverage gaps fail the comparison naturally.
|
|
func (t *translator) hasIEEEF16TableAt(base uint32) bool {
|
|
const tableLen = 65536 * 4
|
|
img := make([]byte, tableLen)
|
|
covered := 0
|
|
placements := t.passiveSegmentPlacements()
|
|
for segIdx, ds := range t.mod.Datas {
|
|
var off int64
|
|
if ds.Passive {
|
|
// Shared-memory (threads) links make every segment
|
|
// passive; the recovered start-section placement stands in
|
|
// for the active-segment offset (see
|
|
// passiveSegmentPlacements).
|
|
po, ok := placements[segIdx]
|
|
if !ok {
|
|
continue
|
|
}
|
|
off = po
|
|
} else {
|
|
eo, err := evalConstExprI64(ds.Offset, t.mod)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
off = eo
|
|
}
|
|
segEnd := off + int64(len(ds.Bytes))
|
|
lo, hi := int64(base), int64(base)+tableLen
|
|
if segEnd <= lo || off >= hi {
|
|
continue
|
|
}
|
|
s, e := max64(off, lo), min64(segEnd, hi)
|
|
copy(img[s-lo:e-lo], ds.Bytes[s-off:e-off])
|
|
covered += int(e - s)
|
|
}
|
|
if covered == 0 {
|
|
return false
|
|
}
|
|
for i := 0; i < 65536; i++ {
|
|
if binary.LittleEndian.Uint32(img[i*4:]) != f16BitsToF32Bits(uint16(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkStaleF16Table warns, once translation is done, about f16
|
|
// gather sites whose base verified NEITHER statically nor through
|
|
// init-loop detection: the table-keyed rewrites stayed off there — a
|
|
// pure performance loss that is otherwise invisible. The warning is
|
|
// unconditional (no configuration gates it) so a detection miss
|
|
// after a module change is loud in every pipeline log.
|
|
func (t *translator) checkStaleF16Table() error {
|
|
if msg := unverifiedF16GatherMsg(t.opts.DisableF16Table, t.f16TablesOK); msg != "" {
|
|
fmt.Fprintln(os.Stderr, msg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// unverifiedF16GatherMsg is checkStaleF16Table's decision: empty
|
|
// means the rewrites are disabled, no gather sites exist, or every
|
|
// queried base verified.
|
|
func unverifiedF16GatherMsg(disabled bool, tables map[uint32]bool) string {
|
|
if disabled {
|
|
return ""
|
|
}
|
|
var unverified []uint32
|
|
for base, ok := range tables {
|
|
if !ok {
|
|
unverified = append(unverified, base)
|
|
}
|
|
}
|
|
if len(unverified) == 0 {
|
|
return ""
|
|
}
|
|
sort.Slice(unverified, func(i, j int) bool { return unverified[i] < unverified[j] })
|
|
return fmt.Sprintf(
|
|
"wasm2go: f16 gather sites at bases %v were not verified (no static table image, no init-loop coverage) — the table-keyed rewrites stayed OFF there",
|
|
unverified)
|
|
}
|
|
|
|
func max64(a, b int64) int64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func min64(a, b int64) int64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// f16TableOK is the cached table check, shaped as the callback the
|
|
// SSA gather pass takes. Two ways a table qualifies:
|
|
//
|
|
// - the module's initial data image holds the IEEE map at base
|
|
// (verified byte-for-byte), or
|
|
// - the module's own code provably initializes the whole range at
|
|
// base (an init loop streaming constant-strided stores — the
|
|
// ggml_init shape; see detectRuntimeTables).
|
|
//
|
|
// There is no external address input: verification is derived from
|
|
// the module alone, and Options.DisableF16Table turns the whole
|
|
// mechanism off.
|
|
func (t *translator) f16TableOK(base uint32) bool {
|
|
if t.opts.DisableF16Table {
|
|
return false
|
|
}
|
|
if t.f16TablesOK == nil {
|
|
t.f16TablesOK = map[uint32]bool{}
|
|
}
|
|
ok, seen := t.f16TablesOK[base]
|
|
if seen {
|
|
return ok
|
|
}
|
|
switch {
|
|
case t.hasIEEEF16TableAt(base):
|
|
ok = true
|
|
default:
|
|
t.detectRuntimeTables()
|
|
if t.runtimeInitCovered(base) {
|
|
ok = true
|
|
t.noteF16TableDetection(base)
|
|
}
|
|
}
|
|
t.f16TablesOK[base] = ok
|
|
return ok
|
|
}
|