File size: 13,063 Bytes
31c7d49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
/** Host-controlled Euler/CFG loop translated from TripoSplat's FlowEulerCfgSampler. */

import { float16BitsToNumber, numberToFloat16Bits } from '../../runtime/float16.ts'

export const TRIPOSPLAT_FAST_FLOW_STEPS = 4
export const TRIPOSPLAT_QUALITY_FLOW_STEPS = 20
export const TRIPOSPLAT_DEFAULT_FLOW_SHIFT = 3
export const TRIPOSPLAT_DEFAULT_GUIDANCE_SCALE = 3

export type FlowTensorState = Record<string, Float32Array>
export type GuidanceScale = number | Readonly<Record<string, number>> | null
export type FlowArithmeticPrecision = 'float32' | 'float16'

export interface ShiftedFlowStep {
  /** Normalized source timestep in [0, 1]. */
  timestep: number
  /** Normalized destination timestep in [0, 1]. */
  previousTimestep: number
  /** Positive Euler interval: timestep - previousTimestep. */
  delta: number
}

export interface FlowModelInvocation<Condition> {
  /** A defensive copy of the current host state. */
  sample: Readonly<FlowTensorState>
  /** Normalized flow timestep before the official x1000 model scaling. */
  timestep: number
  /** `[batch]` float32 tensor containing `1000 * timestep`. */
  timestepTensor: Float32Array
  condition: Condition
  pass: 'conditional' | 'unconditional'
  step: number
  totalSteps: number
}

export type FlowModelPredictor<Condition> = (
  invocation: FlowModelInvocation<Condition>,
) => FlowTensorState | Promise<FlowTensorState>

export interface FlowStepProgress {
  step: number
  totalSteps: number
  timestep: number
  previousTimestep: number
  sample: Readonly<FlowTensorState>
}

export interface FlowSamplerOptions<Condition> {
  condition: Condition
  /** Required only when at least one effective guidance scale is greater than 1. */
  negativeCondition?: Condition
  steps?: number
  shift?: number
  guidanceScale?: GuidanceScale
  /** Precision used by official CFG and velocity scaling; the fp16 DiT requires `float16`. */
  predictionArithmetic?: FlowArithmeticPrecision
  /** TripoSplat image-to-3D inference currently uses one image per invocation. */
  batchSize?: number
  signal?: AbortSignal
  onStep?: (progress: FlowStepProgress) => void
}

function assertPositiveInteger(value: number, label: string): void {
  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`${label} must be a positive integer, got ${value}.`)
  }
}

function throwIfAborted(signal?: AbortSignal): void {
  if (signal?.aborted) {
    throw signal.reason instanceof Error
      ? signal.reason
      : new DOMException('Operation aborted', 'AbortError')
  }
}

function ownEntries(state: Readonly<FlowTensorState>): [string, Float32Array][] {
  return Object.entries(state)
}

export function cloneFlowTensorState(state: Readonly<FlowTensorState>): FlowTensorState {
  const clone: FlowTensorState = {}
  for (const [key, value] of ownEntries(state)) {
    if (!(value instanceof Float32Array)) {
      throw new Error(`Flow tensor '${key}' must be a Float32Array.`)
    }
    clone[key] = new Float32Array(value)
  }
  if (Object.keys(clone).length === 0) {
    throw new Error('Flow state must contain at least one tensor.')
  }
  return clone
}

/** The official schedule transform: shift*t / (1 + (shift - 1)*t). */
export function shiftFlowTimestep(timestep: number, shift: number): number {
  if (!Number.isFinite(timestep) || timestep < 0 || timestep > 1) {
    throw new Error(`timestep must be finite and in [0, 1], got ${timestep}.`)
  }
  if (!Number.isFinite(shift) || shift <= 0) {
    throw new Error(`shift must be a positive finite number, got ${shift}.`)
  }
  return (shift * timestep) / (1 + (shift - 1) * timestep)
}

/**
 * Builds the exact descending schedule used by the official NumPy sampler.
 * A shift of 1 is uniform; values above 1 retain more high-noise timesteps.
 */
export function createShiftedFlowSchedule(
  steps: number,
  shift = TRIPOSPLAT_DEFAULT_FLOW_SHIFT,
): readonly ShiftedFlowStep[] {
  assertPositiveInteger(steps, 'steps')
  if (!Number.isFinite(shift) || shift <= 0) {
    throw new Error(`shift must be a positive finite number, got ${shift}.`)
  }

  const timesteps = new Float64Array(steps + 1)
  // NumPy's descending `linspace(1, 0, steps + 1)` is formed from a negative
  // delta. Keeping that operation order preserves its last-bit schedule values.
  const linearDelta = -1 / steps
  for (let index = 0; index <= steps; index += 1) {
    const linearTimestep = index === steps ? 0 : 1 + index * linearDelta
    timesteps[index] = shiftFlowTimestep(linearTimestep, shift)
  }
  // Avoid a possible signed zero at the terminal endpoint.
  timesteps[steps] = 0

  const schedule: ShiftedFlowStep[] = new Array(steps)
  for (let index = 0; index < steps; index += 1) {
    const timestep = timesteps[index]
    const previousTimestep = timesteps[index + 1]
    schedule[index] = {
      timestep,
      previousTimestep,
      delta: timestep - previousTimestep,
    }
  }
  return schedule
}

function guidanceForKey(guidanceScale: GuidanceScale | undefined, key: string): number {
  if (guidanceScale === undefined || guidanceScale === null) return 1
  if (typeof guidanceScale === 'number') return guidanceScale
  return guidanceScale[key] ?? 1
}

export function usesClassifierFreeGuidance(guidanceScale: GuidanceScale | undefined): boolean {
  if (guidanceScale === undefined || guidanceScale === null) return false
  if (typeof guidanceScale === 'number') {
    if (!Number.isFinite(guidanceScale)) {
      throw new Error(`guidanceScale must be finite, got ${guidanceScale}.`)
    }
    return guidanceScale > 1
  }
  for (const [key, scale] of Object.entries(guidanceScale)) {
    if (!Number.isFinite(scale)) {
      throw new Error(`guidanceScale['${key}'] must be finite, got ${scale}.`)
    }
    if (scale > 1) return true
  }
  return false
}

function assertPredictionMatchesSample(
  prediction: Readonly<FlowTensorState>,
  sample: Readonly<FlowTensorState>,
  label: string,
): void {
  for (const [key, sampleTensor] of ownEntries(sample)) {
    const predictionTensor = prediction[key]
    if (!(predictionTensor instanceof Float32Array)) {
      throw new Error(`${label} is missing Float32Array tensor '${key}'.`)
    }
    if (predictionTensor.length !== sampleTensor.length) {
      throw new Error(
        `${label} tensor '${key}' has ${predictionTensor.length} values; ` +
          `expected ${sampleTensor.length}.`,
      )
    }
  }
}

/**
 * Diffusers-style CFG from the official implementation:
 * `scale * conditional - (scale - 1) * unconditional`.
 */
export function blendClassifierFreeGuidance(
  conditional: Readonly<FlowTensorState>,
  unconditional: Readonly<FlowTensorState>,
  guidanceScale: Exclude<GuidanceScale, null>,
  arithmetic: FlowArithmeticPrecision = 'float32',
): FlowTensorState {
  const blended: FlowTensorState = {}
  for (const [key, conditionalTensor] of ownEntries(conditional)) {
    const scale = guidanceForKey(guidanceScale, key)
    if (!Number.isFinite(scale)) {
      throw new Error(`guidanceScale['${key}'] must be finite, got ${scale}.`)
    }
    if (scale <= 1) {
      blended[key] = new Float32Array(conditionalTensor)
      continue
    }
    const unconditionalTensor = unconditional[key]
    if (!(unconditionalTensor instanceof Float32Array)) {
      throw new Error(`Unconditional prediction is missing Float32Array tensor '${key}'.`)
    }
    if (unconditionalTensor.length !== conditionalTensor.length) {
      throw new Error(
        `Unconditional tensor '${key}' has ${unconditionalTensor.length} values; ` +
          `expected ${conditionalTensor.length}.`,
      )
    }
    const output = new Float32Array(conditionalTensor.length)
    for (let index = 0; index < output.length; index += 1) {
      if (arithmetic === 'float16') {
        const conditionalScaled = roundFloat16(scale * conditionalTensor[index])
        const unconditionalScaled = roundFloat16(
          (scale - 1) * unconditionalTensor[index],
        )
        output[index] = roundFloat16(conditionalScaled - unconditionalScaled)
      } else {
        const conditionalScaled = Math.fround(Math.fround(scale) * conditionalTensor[index])
        const unconditionalScaled = Math.fround(
          Math.fround(scale - 1) * unconditionalTensor[index],
        )
        output[index] = Math.fround(conditionalScaled - unconditionalScaled)
      }
    }
    blended[key] = output
  }
  return blended
}

function roundFloat16(value: number): number {
  return float16BitsToNumber(numberToFloat16Bits(value))
}

function scaleVelocity(
  value: number,
  delta: number,
  arithmetic: FlowArithmeticPrecision,
): number {
  return arithmetic === 'float16'
    ? roundFloat16(value * delta)
    : Math.fround(value * Math.fround(delta))
}

function makeModelTimestep(timestep: number, batchSize: number): Float32Array {
  const modelTimestep = new Float32Array(batchSize)
  modelTimestep.fill(1000 * timestep)
  return modelTimestep
}

export class FlowEulerCfgSampler<Condition> {
  private readonly predictor: FlowModelPredictor<Condition>

  constructor(predictor: FlowModelPredictor<Condition>) {
    this.predictor = predictor
  }

  async sample(
    noise: Readonly<FlowTensorState>,
    options: FlowSamplerOptions<Condition>,
  ): Promise<FlowTensorState> {
    const steps = options.steps ?? TRIPOSPLAT_QUALITY_FLOW_STEPS
    const shift = options.shift ?? TRIPOSPLAT_DEFAULT_FLOW_SHIFT
    const guidanceScale = options.guidanceScale ?? TRIPOSPLAT_DEFAULT_GUIDANCE_SCALE
    const predictionArithmetic = options.predictionArithmetic ?? 'float32'
    const batchSize = options.batchSize ?? 1
    assertPositiveInteger(batchSize, 'batchSize')
    const schedule = createShiftedFlowSchedule(steps, shift)
    const needsUnconditional = usesClassifierFreeGuidance(guidanceScale)
    if (needsUnconditional && !Object.prototype.hasOwnProperty.call(options, 'negativeCondition')) {
      throw new Error('negativeCondition is required when guidanceScale is greater than 1.')
    }

    // Numerically equivalent to official `sample = noise`, without mutating the caller's buffers.
    const sample = cloneFlowTensorState(noise)
    for (let index = 0; index < schedule.length; index += 1) {
      throwIfAborted(options.signal)
      const interval = schedule[index]
      const invocationBase = {
        timestep: interval.timestep,
        timestepTensor: makeModelTimestep(interval.timestep, batchSize),
        step: index + 1,
        totalSteps: schedule.length,
      }
      const conditional = await this.predictor({
        ...invocationBase,
        sample: cloneFlowTensorState(sample),
        condition: options.condition,
        pass: 'conditional',
      })
      throwIfAborted(options.signal)
      assertPredictionMatchesSample(conditional, sample, 'Conditional prediction')

      let prediction = conditional
      if (needsUnconditional) {
        const unconditional = await this.predictor({
          ...invocationBase,
          sample: cloneFlowTensorState(sample),
          condition: options.negativeCondition as Condition,
          pass: 'unconditional',
        })
        throwIfAborted(options.signal)
        assertPredictionMatchesSample(unconditional, sample, 'Unconditional prediction')
        prediction = blendClassifierFreeGuidance(
          conditional,
          unconditional,
          guidanceScale,
          predictionArithmetic,
        )
      }

      // Official Euler update: sample = sample - velocity * (t - t_previous).
      for (const [key, sampleTensor] of ownEntries(sample)) {
        const velocity = prediction[key]
        for (let element = 0; element < sampleTensor.length; element += 1) {
          sampleTensor[element] = Math.fround(
            sampleTensor[element] - scaleVelocity(
              velocity[element],
              interval.delta,
              predictionArithmetic,
            ),
          )
        }
      }
      throwIfAborted(options.signal)
      options.onStep?.({
        step: index + 1,
        totalSteps: schedule.length,
        timestep: interval.timestep,
        previousTimestep: interval.previousTimestep,
        sample,
      })
    }
    return sample
  }
}

export function sampleFlowEulerCfg<Condition>(
  predictor: FlowModelPredictor<Condition>,
  noise: Readonly<FlowTensorState>,
  options: FlowSamplerOptions<Condition>,
): Promise<FlowTensorState> {
  return new FlowEulerCfgSampler(predictor).sample(noise, options)
}

export function sampleFlow4Steps<Condition>(
  predictor: FlowModelPredictor<Condition>,
  noise: Readonly<FlowTensorState>,
  options: Omit<FlowSamplerOptions<Condition>, 'steps'>,
): Promise<FlowTensorState> {
  return sampleFlowEulerCfg(predictor, noise, { ...options, steps: TRIPOSPLAT_FAST_FLOW_STEPS })
}

export function sampleFlow20Steps<Condition>(
  predictor: FlowModelPredictor<Condition>,
  noise: Readonly<FlowTensorState>,
  options: Omit<FlowSamplerOptions<Condition>, 'steps'>,
): Promise<FlowTensorState> {
  return sampleFlowEulerCfg(predictor, noise, { ...options, steps: TRIPOSPLAT_QUALITY_FLOW_STEPS })
}