Yosun's picture
Deploy TripoSplat WebGPU demo
31c7d49 verified
Raw
History Blame Contribute Delete
8.15 kB
import { throwIfAborted } from './errors.js'
import { float16BitsToNumber, numberToFloat16Bits } from './tensors.js'
export const FAST_FLOW_STEPS = 4
export const QUALITY_FLOW_STEPS = 20
export const DEFAULT_FLOW_SHIFT = 3
export const DEFAULT_GUIDANCE_SCALE = 3
export type FlowState = Record<string, Float32Array>
export type FlowCondition = unknown
export type FlowArithmetic = 'fp16' | 'fp32'
export interface FlowInvocation<Condition = FlowCondition> {
sample: Readonly<FlowState>
timestep: number
/** Official model input: timestep multiplied by 1000. */
timestepTensor: Float32Array
condition: Condition
pass: 'conditional' | 'unconditional'
step: number
totalSteps: number
invocation: number
totalInvocations: number
}
export type FlowPredictor<Condition = FlowCondition> = (
invocation: FlowInvocation<Condition>,
) => FlowState | Promise<FlowState>
export interface SamplerOptions<Condition = FlowCondition> {
condition: Condition
negativeCondition?: Condition
steps?: number
shift?: number
guidanceScale?: number | Readonly<Record<string, number>> | null
arithmetic?: FlowArithmetic
signal?: AbortSignal
onStep?: (progress: {
step: number
totalSteps: number
timestep: number
previousTimestep: number
sample: Readonly<FlowState>
}) => void
}
export interface FlowSampler<Condition = FlowCondition> {
sample(noise: Readonly<FlowState>, options: SamplerOptions<Condition>): Promise<FlowState>
}
export interface ShiftedFlowStep {
timestep: number
previousTimestep: number
delta: number
}
function positiveInteger(value: number, label: string): void {
if (!Number.isInteger(value) || value < 1) throw new RangeError(`${label} must be a positive integer.`)
}
export function shiftedFlowTimestep(timestep: number, shift: number): number {
if (!Number.isFinite(timestep) || timestep < 0 || timestep > 1) {
throw new RangeError('timestep must be in [0, 1].')
}
if (!Number.isFinite(shift) || shift <= 0) throw new RangeError('shift must be positive.')
return shift * timestep / (1 + (shift - 1) * timestep)
}
export function createFlowSchedule(steps: number, shift = DEFAULT_FLOW_SHIFT): ShiftedFlowStep[] {
positiveInteger(steps, 'steps')
const values = new Float64Array(steps + 1)
for (let index = 0; index <= steps; index += 1) {
const linear = index === steps ? 0 : 1 + index * (-1 / steps)
values[index] = shiftedFlowTimestep(linear, shift)
}
values[steps] = 0
return Array.from({ length: steps }, (_, index) => ({
timestep: values[index],
previousTimestep: values[index + 1],
delta: values[index] - values[index + 1],
}))
}
export function cloneFlowState(state: Readonly<FlowState>): FlowState {
const clone: FlowState = {}
for (const [name, values] of Object.entries(state)) {
if (!(values instanceof Float32Array)) throw new TypeError(`Flow state '${name}' must be Float32Array.`)
clone[name] = new Float32Array(values)
}
if (Object.keys(clone).length === 0) throw new TypeError('Flow state must not be empty.')
return clone
}
function roundFp16(value: number): number {
return float16BitsToNumber(numberToFloat16Bits(value))
}
function guidanceFor(scale: SamplerOptions['guidanceScale'], name: string): number {
if (scale === undefined || scale === null) return 1
return typeof scale === 'number' ? scale : scale[name] ?? 1
}
function usesGuidance(scale: SamplerOptions['guidanceScale']): boolean {
if (scale === undefined || scale === null) return false
if (typeof scale === 'number') return scale > 1
return Object.values(scale).some((value) => value > 1)
}
function validatePrediction(prediction: Readonly<FlowState>, sample: Readonly<FlowState>, label: string): void {
for (const [name, values] of Object.entries(sample)) {
if (!(prediction[name] instanceof Float32Array) || prediction[name].length !== values.length) {
throw new TypeError(`${label} tensor '${name}' does not match the sample.`)
}
}
}
export function blendGuidance(
conditional: Readonly<FlowState>,
unconditional: Readonly<FlowState>,
scale: NonNullable<SamplerOptions['guidanceScale']>,
arithmetic: FlowArithmetic = 'fp32',
): FlowState {
const result: FlowState = {}
for (const [name, values] of Object.entries(conditional)) {
const strength = guidanceFor(scale, name)
if (!Number.isFinite(strength)) throw new TypeError(`Guidance for '${name}' is not finite.`)
const negative = unconditional[name]
if (!(negative instanceof Float32Array) || negative.length !== values.length) {
throw new TypeError(`Unconditional tensor '${name}' does not match the conditional tensor.`)
}
const blended = new Float32Array(values.length)
for (let index = 0; index < values.length; index += 1) {
if (strength <= 1) blended[index] = values[index]
else if (arithmetic === 'fp16') {
blended[index] = roundFp16(
roundFp16(strength * values[index]) - roundFp16((strength - 1) * negative[index]),
)
} else {
const positive = Math.fround(Math.fround(strength) * values[index])
const negativeScaled = Math.fround(Math.fround(strength - 1) * negative[index])
blended[index] = Math.fround(positive - negativeScaled)
}
}
result[name] = blended
}
return result
}
class EulerCfgSampler<Condition> implements FlowSampler<Condition> {
constructor(private readonly predictor: FlowPredictor<Condition>) {}
async sample(noise: Readonly<FlowState>, options: SamplerOptions<Condition>): Promise<FlowState> {
const steps = options.steps ?? QUALITY_FLOW_STEPS
const schedule = createFlowSchedule(steps, options.shift ?? DEFAULT_FLOW_SHIFT)
const scale = options.guidanceScale ?? DEFAULT_GUIDANCE_SCALE
const guided = usesGuidance(scale)
if (guided && !Object.prototype.hasOwnProperty.call(options, 'negativeCondition')) {
throw new TypeError('negativeCondition is required for classifier-free guidance.')
}
const arithmetic = options.arithmetic ?? 'fp16'
const state = cloneFlowState(noise)
const totalInvocations = steps * (guided ? 2 : 1)
let invocation = 0
for (const [index, interval] of schedule.entries()) {
throwIfAborted(options.signal)
const shared = {
timestep: interval.timestep,
timestepTensor: Float32Array.of(interval.timestep * 1000),
step: index + 1,
totalSteps: steps,
totalInvocations,
}
invocation += 1
const conditional = await this.predictor({
...shared,
invocation,
sample: cloneFlowState(state),
condition: options.condition,
pass: 'conditional',
})
validatePrediction(conditional, state, 'Conditional prediction')
let velocity = conditional
if (guided) {
throwIfAborted(options.signal)
invocation += 1
const unconditional = await this.predictor({
...shared,
invocation,
sample: cloneFlowState(state),
condition: options.negativeCondition as Condition,
pass: 'unconditional',
})
validatePrediction(unconditional, state, 'Unconditional prediction')
velocity = blendGuidance(conditional, unconditional, scale, arithmetic)
}
for (const [name, values] of Object.entries(state)) {
const prediction = velocity[name]
for (let element = 0; element < values.length; element += 1) {
const delta = arithmetic === 'fp16'
? roundFp16(prediction[element] * interval.delta)
: Math.fround(prediction[element] * Math.fround(interval.delta))
values[element] = Math.fround(values[element] - delta)
}
}
throwIfAborted(options.signal)
options.onStep?.({
step: index + 1,
totalSteps: steps,
timestep: interval.timestep,
previousTimestep: interval.previousTimestep,
sample: state,
})
}
return state
}
}
export function createSampler<Condition>(predictor: FlowPredictor<Condition>): FlowSampler<Condition> {
return new EulerCfgSampler(predictor)
}