| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { runEncoder } from './encoder.js'; |
| import { createDecodeState, encodeDecodeStep } from './decoder.js'; |
| import { |
| createUniformParamPool, getDispatchStats, shouldUseUniformParamPool, |
| } from './pipelines.js'; |
| import { maxBatchForLimits } from './shapes.js'; |
| import { EOS, VOCAB } from './constants.js'; |
|
|
| const ALL5 = ['fc1', 'fc2', 'self_out', 'cross_q', 'cross_out']; |
|
|
| |
| export const DEFAULT_ROUTING = Object.freeze({ |
| fuseLnMaxB: 4, |
| lmHeadMinB: 16, |
| lmHeadFuse: 'auto', |
| projTiledMinB: 128, |
| projTiledKinds: ['fc1', 'fc2'], |
| |
| |
| |
| |
| decodeMega: 'auto', |
| |
| |
| |
| |
| sg: 'off', |
| |
| |
| |
| |
| |
| |
| |
| |
| ffnSkLarge: 'auto', |
| }); |
|
|
| |
| |
| export const FFN_SK_OFF_MIN_B = 256; |
|
|
| |
| |
| const SK_PROBE_S = 64; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function resolveSkProbeB(ctx, dtypeBytes, maxB) { |
| const bindingB = Math.floor(maxBatchForLimits(ctx, SK_PROBE_S, dtypeBytes) / 64) * 64; |
| const b = Math.min(576, maxB ?? 576, bindingB); |
| return b >= FFN_SK_OFF_MIN_B ? b : null; |
| } |
|
|
| |
| |
| |
| |
| |
| const WIN_MARGIN = 0.95; |
|
|
| |
| |
| |
| export const KERNEL_REV = 13; |
|
|
| |
| |
| |
| export const AUTOTUNE_PROTOCOL_REV = 2; |
|
|
| const median = (xs) => { |
| const s = [...xs].sort((a, b) => a - b); |
| const m = s.length >> 1; |
| return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; |
| }; |
|
|
| export class ReductionSafetyError extends Error { |
| constructor(message, verdicts) { |
| super(message); |
| this.name = 'ReductionSafetyError'; |
| this.code = 'WEBMT_NO_SAFE_REDUCTION'; |
| this.verdicts = verdicts; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function resolveReductionSafety({ treeBug, sgMismap, sgFeature }) { |
| const valid = (v) => v === true || v === false || v === null; |
| if (!valid(treeBug) || !valid(sgMismap)) { |
| throw new Error(`invalid reduction verdicts: treeBug=${treeBug}, sgMismap=${sgMismap}`); |
| } |
| const treeSafe = treeBug === false; |
| const sgSafe = !!sgFeature && sgMismap === false; |
| if (treeSafe && sgSafe) { |
| return { status: 'both-safe', retry: false, sgOk: true, forceSg: false }; |
| } |
| if (treeSafe) { |
| return { status: 'tree-safe', retry: false, sgOk: false, forceSg: false }; |
| } |
| if (sgSafe) { |
| return { status: 'subgroup-safe', retry: false, sgOk: true, forceSg: true }; |
| } |
| const retry = treeBug === null || (!!sgFeature && sgMismap === null); |
| return { status: retry ? 'unresolved' : 'unsafe', retry, sgOk: false, forceSg: false }; |
| } |
|
|
| |
| |
| export function rotatedArmOrder(names, round) { |
| if (!names.length) return []; |
| const offset = ((round % names.length) + names.length) % names.length; |
| return [...names.slice(offset), ...names.slice(0, offset)]; |
| } |
|
|
| export function pairedRatios(runs, alt, base = 'def') { |
| const a = runs[alt]; |
| const b = runs[base]; |
| if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || !a.length) { |
| throw new Error(`pairedRatios: ${alt}/${base} need equal non-empty run arrays`); |
| } |
| return a.map((v, i) => v / b[i]); |
| } |
|
|
| |
| |
| |
| |
| export function pairedPerfVerdict(runs, alt, base = 'def') { |
| const ratios = pairedRatios(runs, alt, base); |
| const med = median(ratios); |
| const favorable = ratios.filter((r) => r < 1).length; |
| if (ratios.length <= 3) { |
| |
| |
| if (med <= 0.90 && favorable === ratios.length) return 'win'; |
| if (med >= 0.98 && Math.min(...ratios) >= 0.95) return 'keep'; |
| return 'more'; |
| } |
| const need = Math.ceil(ratios.length * (2 / 3)); |
| return med < WIN_MARGIN && favorable >= need ? 'win' : 'keep'; |
| } |
|
|
| |
| |
| |
| |
| export function pairedStableSgWin(runs, alt = 'sgOn', base = 'def') { |
| const ratios = pairedRatios(runs, alt, base); |
| if (ratios.length < 7) return false; |
| const favorable = ratios.filter((ratio) => ratio < 1).length; |
| return median(ratios) <= 0.90 && favorable === ratios.length; |
| } |
|
|
| |
| |
| function syntheticBatch(B, S) { |
| const ids = new Uint32Array(B * S); |
| let x = 0x9e3779b9; |
| for (let i = 0; i < ids.length; i++) { |
| x = (Math.imul(x, 1664525) + 1013904223) >>> 0; |
| ids[i] = 100 + (x % (VOCAB - 200)); |
| } |
| for (let b = 0; b < B; b++) ids[b * S + S - 1] = EOS; |
| return { ids, lens: new Uint32Array(B).fill(S), B, S }; |
| } |
|
|
| |
| |
| |
| function createAutotuneParamPoolSession(ctx, enabled) { |
| let pool = null; |
| let destroyed = false; |
| let decodeTransientUniforms = 0; |
| return { |
| poolFor(state) { |
| if (destroyed) throw new Error('autotune uniform-pool session is destroyed'); |
| if (!shouldUseUniformParamPool(enabled, state)) return null; |
| pool ??= createUniformParamPool(ctx.device, { banks: 2 }); |
| return pool; |
| }, |
| recordDecodeUniformDelta(delta) { |
| decodeTransientUniforms += Math.max(0, delta); |
| }, |
| invalidateBindings() { |
| return pool?.invalidateBindings() ?? 0; |
| }, |
| snapshot() { |
| return { decodeTransientUniforms }; |
| }, |
| destroy() { |
| if (destroyed) return; |
| pool?.destroy(); |
| pool = null; |
| destroyed = true; |
| }, |
| }; |
| } |
|
|
| |
| |
| |
| async function timeArm(ctx, weights, encRun, B, steps, opts, paramPoolSession) { |
| const { device } = ctx; |
| const state = createDecodeState(ctx, weights, { |
| B, S: encRun.S, maxSteps: steps, ...opts, |
| }); |
| let uniformsBefore = null; |
| let armDrained = false; |
| try { |
| |
| |
| |
| const paramPool = paramPoolSession.poolFor(state); |
| if (paramPool) uniformsBefore = getDispatchStats(device).uniformBuffersCreated; |
| const t0 = performance.now(); |
| for (let g = 0; g < steps; g += 8) { |
| const tEnd = Math.min(g + 8, steps); |
| const bank = (g / 8) % 2; |
| let poolFrameActive = false; |
| const scratch = []; |
| try { |
| if (paramPool) { |
| paramPool.begin(bank); |
| poolFrameActive = true; |
| } |
| const encoder = device.createCommandEncoder({ label: `autotune ${g}..${tEnd}` }); |
| const pass = encoder.beginComputePass(); |
| for (let t = g; t < tEnd; t++) { |
| scratch.push(...encodeDecodeStep(ctx, weights, encRun, state, t, pass).scratch); |
| } |
| pass.end(); |
| if (paramPool) { |
| paramPool.flush(); |
| poolFrameActive = false; |
| } |
| device.queue.submit([encoder.finish()]); |
| if (paramPool) paramPool.release(bank); |
| } catch (err) { |
| if (poolFrameActive) paramPool.abort(); |
| throw err; |
| } finally { |
| for (const b of scratch) b.destroy(); |
| } |
| } |
| await device.queue.onSubmittedWorkDone(); |
| armDrained = true; |
| return Number((((performance.now() - t0) * 1000) / steps).toFixed(1)); |
| } finally { |
| try { |
| if (uniformsBefore !== null) { |
| const uniformsAfter = getDispatchStats(device).uniformBuffersCreated; |
| paramPoolSession.recordDecodeUniformDelta(uniformsAfter - uniformsBefore); |
| } |
| } finally { |
| try { |
| |
| |
| |
| |
| if (armDrained) paramPoolSession.invalidateBindings(); |
| } finally { |
| state.destroy(); |
| } |
| } |
| } |
| } |
|
|
| export function cachedAutotuneMatches(cachedUs, currentUs, tolerance = 0.15) { |
| if (!(Number.isFinite(cachedUs) && cachedUs > 0 && Number.isFinite(currentUs) && currentUs > 0)) { |
| return false; |
| } |
| return Math.abs((currentUs / cachedUs) - 1) <= tolerance + Number.EPSILON; |
| } |
|
|
| export function cachedPolicyMatchesSafety(tuned, safety) { |
| if (!tuned || !safety) return false; |
| if (safety.forceSg) return tuned.sg === 'on' && tuned.sgForced === true; |
| if (!safety.sgOk) return tuned.sg !== 'on'; |
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| export async function measureAutotuneReference( |
| ctx, |
| weights, |
| { |
| B = 64, S = 96, steps = 24, rounds = 3, |
| immediates = 'auto', uniformPool = false, |
| } = {}, |
| ) { |
| const batch = syntheticBatch(B, S); |
| const encRun = await runEncoder(ctx, weights, batch, { retainEncOut: false }); |
| await ctx.device.queue.onSubmittedWorkDone(); |
| const paramPoolSession = createAutotuneParamPoolSession(ctx, uniformPool); |
| try { |
| await timeArm(ctx, weights, encRun, B, steps, { immediates }, paramPoolSession); |
| const runs = []; |
| for (let r = 0; r < rounds; r++) { |
| runs.push(await timeArm(ctx, weights, encRun, B, steps, { immediates }, paramPoolSession)); |
| } |
| return { medianUs: median(runs), runs, poolStats: paramPoolSession.snapshot() }; |
| } finally { |
| try { |
| paramPoolSession.destroy(); |
| } finally { |
| encRun.arena.destroy(); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function detectTreeReductionBug(device) { |
| const D = 448; |
| const WG = 256; |
| const G = 8; |
| const code = ` |
| @group(0) @binding(0) var<storage, read_write> out: array<f32>; |
| const D: u32 = ${D}u; |
| const WG: u32 = ${WG}u; |
| fn vhash(i: u32, g: u32) -> f32 { |
| return f32((((i + 1u) * 2654435761u) ^ ((g + 1u) * 40503u)) & 1023u); |
| } |
| var<workgroup> vbuf: array<f32, D>; |
| var<workgroup> scratch: array<f32, WG>; |
| @compute @workgroup_size(${WG}) |
| fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) { |
| let g = wid.x; let tid = lid.x; |
| var sum: f32 = 0.0; |
| for (var i = tid; i < D; i = i + WG) { |
| let v = (vhash(i, g) - 512.0) / 256.0 + (vhash(i + 7777u, g) - 512.0) / 256.0; |
| vbuf[i] = v; |
| sum = sum + v; |
| } |
| scratch[tid] = sum; |
| workgroupBarrier(); |
| for (var s = WG / 2u; s > 0u; s = s >> 1u) { |
| if (tid < s) { scratch[tid] = scratch[tid] + scratch[tid + s]; } |
| workgroupBarrier(); |
| } |
| let mu = scratch[0] / f32(D); |
| workgroupBarrier(); |
| for (var i = tid; i < D; i = i + WG) { out[g * D + i] = vbuf[i] - mu; } |
| }`; |
| try { |
| const module = device.createShaderModule({ code }); |
| const pipeline = device.createComputePipeline({ |
| layout: 'auto', compute: { module, entryPoint: 'main' }, |
| }); |
| const outBuf = device.createBuffer({ |
| size: G * D * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, |
| }); |
| const staging = device.createBuffer({ |
| size: G * D * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, |
| }); |
| const bindGroup = device.createBindGroup({ |
| layout: pipeline.getBindGroupLayout(0), |
| entries: [{ binding: 0, resource: { buffer: outBuf } }], |
| }); |
| const encoder = device.createCommandEncoder(); |
| const pass = encoder.beginComputePass(); |
| pass.setPipeline(pipeline); |
| pass.setBindGroup(0, bindGroup); |
| pass.dispatchWorkgroups(G); |
| pass.end(); |
| encoder.copyBufferToBuffer(outBuf, 0, staging, 0, G * D * 4); |
| device.queue.submit([encoder.finish()]); |
| await staging.mapAsync(GPUMapMode.READ); |
| const got = new Float32Array(staging.getMappedRange().slice(0)); |
| staging.unmap(); |
| outBuf.destroy(); |
| staging.destroy(); |
| const hash = (i, g) => ((Math.imul(i + 1, 2654435761) ^ Math.imul(g + 1, 40503)) >>> 0) & 1023; |
| for (let g = 0; g < G; g++) { |
| const v = new Float64Array(D); |
| let sum = 0; |
| for (let i = 0; i < D; i++) { |
| v[i] = (hash(i, g) - 512) / 256 + (hash(i + 7777, g) - 512) / 256; |
| sum += v[i]; |
| } |
| const mu = sum / D; |
| for (let i = 0; i < D; i++) { |
| if (!(Math.abs(got[g * D + i] - (v[i] - mu)) <= 1e-3)) return true; |
| } |
| } |
| return false; |
| } catch { |
| |
| |
| |
| |
| |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function detectSgShuffleMismap(device, maxTk) { |
| const WG = 64; |
| const G = 4; |
| const tks = [4, 8, 16, 32].filter((tk) => tk <= Math.min(maxTk ?? 0, WG)); |
| if (!tks.length) return null; |
| const hash = (i, g) => (((Math.imul(i + 1, 2654435761) ^ Math.imul(g + 1, 40503)) >>> 0) & 1023) / 64; |
| try { |
| for (const TK of tks) { |
| const slices = WG / TK; |
| const code = ` |
| enable subgroups; |
| @group(0) @binding(0) var<storage, read_write> out: array<f32>; |
| const TK: u32 = ${TK}u; |
| fn vhash(i: u32, g: u32) -> f32 { |
| return f32((((i + 1u) * 2654435761u) ^ ((g + 1u) * 40503u)) & 1023u) / 64.0; |
| } |
| @compute @workgroup_size(${WG}) |
| fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) { |
| let g = wid.x; let tid = lid.x; |
| var vr = vhash(tid, g); |
| for (var s = TK / 2u; s > 0u; s = s >> 1u) { |
| vr = vr + subgroupShuffleDown(vr, s); |
| } |
| if (tid % TK == 0u) { out[g * ${slices}u + tid / TK] = vr; } |
| }`; |
| const module = device.createShaderModule({ code }); |
| const pipeline = device.createComputePipeline({ |
| layout: 'auto', compute: { module, entryPoint: 'main' }, |
| }); |
| const outBuf = device.createBuffer({ |
| size: G * slices * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, |
| }); |
| const staging = device.createBuffer({ |
| size: G * slices * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, |
| }); |
| const bindGroup = device.createBindGroup({ |
| layout: pipeline.getBindGroupLayout(0), |
| entries: [{ binding: 0, resource: { buffer: outBuf } }], |
| }); |
| const encoder = device.createCommandEncoder(); |
| const pass = encoder.beginComputePass(); |
| pass.setPipeline(pipeline); |
| pass.setBindGroup(0, bindGroup); |
| pass.dispatchWorkgroups(G); |
| pass.end(); |
| encoder.copyBufferToBuffer(outBuf, 0, staging, 0, G * slices * 4); |
| device.queue.submit([encoder.finish()]); |
| await staging.mapAsync(GPUMapMode.READ); |
| const got = new Float32Array(staging.getMappedRange().slice(0)); |
| staging.unmap(); |
| outBuf.destroy(); |
| staging.destroy(); |
| for (let g = 0; g < G; g++) { |
| for (let sl = 0; sl < slices; sl++) { |
| let want = 0; |
| for (let i = 0; i < TK; i++) want += hash(sl * TK + i, g); |
| if (!(Math.abs(got[g * slices + sl] - want) <= 1e-2)) return true; |
| } |
| } |
| } |
| return false; |
| } catch { |
| |
| |
| |
| return null; |
| } |
| } |
|
|
| export async function probeReductionSafety( |
| ctx, |
| { treeProbe = detectTreeReductionBug, sgProbe = detectSgShuffleMismap } = {}, |
| ) { |
| const sgFeature = !!ctx.hasSubgroups && (ctx.subgroupMinSize ?? 0) >= 16; |
| |
| |
| |
| |
| |
| |
| |
| const probe = async () => { |
| const treeBug = await treeProbe(ctx.device); |
| |
| const sgMismap = sgFeature |
| ? await sgProbe(ctx.device, ctx.subgroupMinSize) |
| : true; |
| return { treeBug, sgMismap }; |
| }; |
| let verdicts = await probe(); |
| let safety = resolveReductionSafety({ ...verdicts, sgFeature }); |
| if (safety.retry) { |
| await ctx.device.queue.onSubmittedWorkDone(); |
| verdicts = await probe(); |
| safety = resolveReductionSafety({ ...verdicts, sgFeature }); |
| } |
| if (safety.status === 'unsafe' || safety.status === 'unresolved') { |
| throw new ReductionSafetyError( |
| `webMT: no proven-safe GPU reduction route ` + |
| `(treeBug=${verdicts.treeBug}, sgMismap=${verdicts.sgMismap}, subgroups=${sgFeature})`, |
| { ...verdicts, sgFeature }, |
| ); |
| } |
| return { ...safety, verdicts: { ...verdicts, sgFeature } }; |
| } |
|
|
| export async function autotuneRouting(ctx, weights, opts = {}) { |
| const { reductionSafety = null, uniformPool = false, ...perfOpts } = opts; |
| const safety = reductionSafety ?? await probeReductionSafety(ctx); |
| const paramPoolSession = createAutotuneParamPoolSession(ctx, uniformPool); |
| try { |
| try { |
| const result = await probeAndDecide(ctx, weights, perfOpts, safety, paramPoolSession); |
| return { ...result, poolStats: paramPoolSession.snapshot() }; |
| } catch (err) { |
| |
| |
| |
| |
| |
| const tuned = { ...DEFAULT_ROUTING, projTiledKinds: [...DEFAULT_ROUTING.projTiledKinds] }; |
| if (safety.forceSg) { |
| tuned.sg = 'on'; |
| tuned.sgForced = true; |
| } |
| return { |
| tuned, |
| timings: {}, |
| partial: String(err?.message ?? err), |
| poolStats: paramPoolSession.snapshot(), |
| }; |
| } |
| } finally { |
| paramPoolSession.destroy(); |
| } |
| } |
|
|
| async function probeAndDecide( |
| ctx, |
| weights, |
| { |
| S = 96, steps = 24, rounds = 3, extraRounds = 4, |
| immediates = 'auto', maxB = null, |
| }, |
| { sgOk, forceSg }, |
| paramPoolSession, |
| ) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const points = [ |
| { |
| B: 1, |
| arms: { |
| def: {}, |
| lnOff: { decodeMega: 'off', fuseLn: 'off' }, |
| lnOn: { decodeMega: 'off', fuseLn: 'on' }, |
| ...(sgOk ? { |
| defSg: { sg: 'on' }, |
| lnOffSg: { decodeMega: 'off', fuseLn: 'off', sg: 'on' }, |
| lnOnSg: { decodeMega: 'off', fuseLn: 'on', sg: 'on' }, |
| } : {}), |
| }, |
| comparisons: [ |
| ['lnOff', 'def'], ['lnOn', 'def'], |
| ...(sgOk ? [['lnOffSg', 'defSg'], ['lnOnSg', 'defSg']] : []), |
| ], |
| }, |
| { B: 16, arms: { def: {}, gemv: { lmHead: 'gemv' } }, comparisons: [['gemv', 'def']] }, |
| { B: 32, arms: { def: {}, all5: { tiledProj: ALL5 } }, comparisons: [['all5', 'def']] }, |
| { |
| B: 64, |
| arms: { def: {}, all5: { tiledProj: ALL5 }, fuseOff: { lmHeadFuse: 'off' } }, |
| comparisons: [['all5', 'def'], ['fuseOff', 'def']], |
| }, |
| ]; |
| |
| |
| |
| |
| |
| const skB = resolveSkProbeB(ctx, weights.dtype === 'f16' ? 2 : 4, maxB); |
| if (skB) { |
| points.push({ |
| B: skB, S: SK_PROBE_S, steps: 12, |
| arms: { def: {}, sk0: { ffnSplitK: 0 } }, |
| comparisons: [['sk0', 'def']], |
| }); |
| } |
| |
| |
| |
| |
| |
| |
| if (sgOk) { |
| points.push({ |
| B: 8, |
| arms: { def: {}, sgOn: { sg: 'on' } }, |
| comparisons: [['sgOn', 'def']], |
| minRounds: rounds + extraRounds, |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| points.sort((a, b) => b.B - a.B); |
|
|
| const timings = {}; |
| for (const point of points) { |
| const batch = syntheticBatch(point.B, point.S ?? S); |
| const pointSteps = point.steps ?? steps; |
| const encRun = await runEncoder(ctx, weights, batch, { retainEncOut: false }); |
| await ctx.device.queue.onSubmittedWorkDone(); |
| try { |
| const names = Object.keys(point.arms); |
| for (const name of names) { |
| await timeArm(ctx, weights, encRun, point.B, pointSteps, { |
| ...point.arms[name], immediates, |
| }, paramPoolSession); |
| } |
| const runs = Object.fromEntries(names.map((n) => [n, []])); |
| const measureRound = async (r) => { |
| const order = rotatedArmOrder(names, r); |
| for (const name of order) { |
| runs[name].push(await timeArm(ctx, weights, encRun, point.B, pointSteps, { |
| ...point.arms[name], immediates, |
| }, paramPoolSession)); |
| } |
| }; |
| const screeningRounds = point.minRounds ?? rounds; |
| for (let r = 0; r < screeningRounds; r++) await measureRound(r); |
| const needsMore = point.comparisons.some(([alt, base]) => |
| pairedPerfVerdict(runs, alt, base) === 'more'); |
| if (needsMore && screeningRounds === rounds) { |
| for (let r = screeningRounds; r < rounds + extraRounds; r++) await measureRound(r); |
| } |
| timings[`b${point.B}`] = Object.fromEntries( |
| names.map((n) => [n, { medianUs: median(runs[n]), runs: runs[n] }]), |
| ); |
| } finally { |
| encRun.arena.destroy(); |
| } |
| } |
|
|
| const wins = (B, alt, base = 'def') => { |
| const runs = Object.fromEntries( |
| Object.entries(timings[`b${B}`]).map(([name, entry]) => [name, entry.runs]), |
| ); |
| return pairedPerfVerdict(runs, alt, base) === 'win'; |
| }; |
|
|
| const tuned = { ...DEFAULT_ROUTING, projTiledKinds: [...DEFAULT_ROUTING.projTiledKinds] }; |
| |
| |
| if (forceSg) { |
| tuned.sg = 'on'; |
| tuned.sgForced = true; |
| } else if (timings.b8) { |
| const b8Runs = Object.fromEntries( |
| Object.entries(timings.b8).map(([name, entry]) => [name, entry.runs]), |
| ); |
| if (pairedStableSgWin(b8Runs)) tuned.sg = 'on'; |
| } |
| const sfx = tuned.sg === 'on' ? 'Sg' : ''; |
| |
| |
| |
| const b1 = timings.b1; |
| const b1Runs = Object.fromEntries(Object.entries(b1).map(([name, entry]) => [name, entry.runs])); |
| const baseName = `def${sfx}`; |
| const offName = `lnOff${sfx}`; |
| const onName = `lnOn${sfx}`; |
| const offRatio = median(pairedRatios(b1Runs, offName, baseName)); |
| const onRatio = median(pairedRatios(b1Runs, onName, baseName)); |
| const leanBest = offRatio <= onRatio ? offName : onName; |
| if (wins(1, leanBest, `def${sfx}`)) { |
| tuned.decodeMega = 'off'; |
| tuned.fuseLnMaxB = leanBest.startsWith('lnOff') ? 0 : DEFAULT_ROUTING.fuseLnMaxB; |
| } |
| if (wins(16, 'gemv')) tuned.lmHeadMinB = 32; |
| if (wins(64, 'fuseOff')) tuned.lmHeadFuse = 'off'; |
| if (wins(32, 'all5')) { |
| tuned.projTiledMinB = 32; |
| tuned.projTiledKinds = [...ALL5]; |
| } else if (wins(64, 'all5')) { |
| tuned.projTiledMinB = 64; |
| tuned.projTiledKinds = [...ALL5]; |
| } |
| if (skB && wins(skB, 'sk0')) tuned.ffnSkLarge = 'off'; |
| return { tuned, timings }; |
| } |
|
|
| |
| |
| export function tunedOptions(tuned, B) { |
| if (!tuned) return {}; |
| return { |
| fuseLn: B <= tuned.fuseLnMaxB ? 'on' : 'off', |
| lmHead: B >= tuned.lmHeadMinB ? 'auto' : 'gemv', |
| lmHeadFuse: tuned.lmHeadFuse === 'off' ? 'off' : 'auto', |
| tiledProj: B >= tuned.projTiledMinB && tuned.projTiledMinB < DEFAULT_ROUTING.projTiledMinB |
| ? [...tuned.projTiledKinds] |
| : 'auto', |
| |
| |
| |
| |
| decodeMega: tuned.sgForced ? 'off' : (tuned.decodeMega === 'off' ? 'off' : 'auto'), |
| |
| |
| sg: tuned.sg === 'on' ? 'on' : 'off', |
| |
| |
| |
| encAttnSafe: !!tuned.sgForced, |
| |
| |
| ffnSplitK: tuned.ffnSkLarge === 'off' && B >= FFN_SK_OFF_MIN_B ? 0 : 'auto', |
| }; |
| } |
|
|