perf: harden realtime localization pipeline
This commit is contained in:
+125
-28
@@ -1,6 +1,11 @@
|
||||
import { dbToLinear, createScanPoints, distance3, findPeak, scanIndexToPosition, TAU } from "./math.js";
|
||||
import { interpolateFrequencyResponse } from "./frequency-response.js";
|
||||
import { binsForBand, extractStftBins, getFrameCount } from "./stft.js";
|
||||
import {
|
||||
interpolateFrequencyResponse,
|
||||
inverseResponseGainDb,
|
||||
requireMaxCompensationDb,
|
||||
} from "./frequency-response.js";
|
||||
import { cooperativeYield } from "./cooperative-yield.js";
|
||||
import { binsForBand, createStftWorkspace, extractStftBins, getFrameCount } from "./stft.js";
|
||||
|
||||
function throwIfAborted(signal) {
|
||||
if (signal?.aborted) {
|
||||
@@ -10,10 +15,6 @@ function throwIfAborted(signal) {
|
||||
}
|
||||
}
|
||||
|
||||
function yieldToEventLoop() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
export const DEFAULT_MAXIMUM_CPU_STEERING_CACHE_BYTES = 64 * 1024 * 1024;
|
||||
export const DEFAULT_MAXIMUM_CPU_ANALYSIS_PLAN_BYTES = 512 * 1024 * 1024;
|
||||
const DAS_ANALYSIS_PLAN_KIND = "micarrsim.das-analysis-plan/1";
|
||||
@@ -47,12 +48,20 @@ function createBandIndexLookup(bandBins) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function createBinCalibration(arrayDefinition, bin, sampleRate, fftSize, compensateFrequencyResponse) {
|
||||
function createBinCalibration(
|
||||
arrayDefinition,
|
||||
bin,
|
||||
sampleRate,
|
||||
fftSize,
|
||||
compensateFrequencyResponse,
|
||||
maximumCompensationDb,
|
||||
) {
|
||||
const microphoneCount = arrayDefinition.microphones.length;
|
||||
const gains = new Float64Array(microphoneCount);
|
||||
const phases = new Float64Array(microphoneCount);
|
||||
const frequencyHz = (bin * sampleRate) / fftSize;
|
||||
const response = {};
|
||||
const maxCompensationDb = requireMaxCompensationDb(maximumCompensationDb);
|
||||
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
|
||||
const microphone = arrayDefinition.microphones[microphoneIndex];
|
||||
let responseGainDb = 0;
|
||||
@@ -62,8 +71,11 @@ function createBinCalibration(arrayDefinition, bin, sampleRate, fftSize, compens
|
||||
responseGainDb = response.gainDb;
|
||||
responsePhaseDeg = response.phaseDeg;
|
||||
}
|
||||
const responseCompensationDb = compensateFrequencyResponse
|
||||
? inverseResponseGainDb(responseGainDb, maxCompensationDb)
|
||||
: 0;
|
||||
gains[microphoneIndex] = dbToLinear(
|
||||
-(microphone.sensitivityDb ?? 0) - responseGainDb,
|
||||
-(microphone.sensitivityDb ?? 0) + responseCompensationDb,
|
||||
);
|
||||
phases[microphoneIndex] =
|
||||
((microphone.phaseOffsetDeg ?? 0) + responsePhaseDeg) * Math.PI / 180;
|
||||
@@ -119,7 +131,9 @@ async function createSteeringMatrix(options) {
|
||||
outputImaginary: imaginary,
|
||||
outputOffset: offset,
|
||||
});
|
||||
if ((pointIndex + 1) % pointBatchSize === 0) await yieldToEventLoop();
|
||||
if ((pointIndex + 1) % pointBatchSize === 0 && pointIndex + 1 < pointCount) {
|
||||
await cooperativeYield();
|
||||
}
|
||||
}
|
||||
return { real, imaginary };
|
||||
}
|
||||
@@ -136,6 +150,7 @@ export async function computeBinMap(options) {
|
||||
fftSize,
|
||||
speedOfSound,
|
||||
compensateFrequencyResponse = true,
|
||||
maxCompensationDb,
|
||||
calibration: providedCalibration,
|
||||
steeringMatrix,
|
||||
output: providedOutput,
|
||||
@@ -156,6 +171,7 @@ export async function computeBinMap(options) {
|
||||
sampleRate,
|
||||
fftSize,
|
||||
compensateFrequencyResponse,
|
||||
maxCompensationDb,
|
||||
);
|
||||
const phaseRealValues = steeringMatrix?.real ?? steeringReal;
|
||||
const phaseImaginaryValues = steeringMatrix?.imaginary ?? steeringImaginary;
|
||||
@@ -212,7 +228,9 @@ export async function computeBinMap(options) {
|
||||
accumulatedPower += (sumReal * sumReal + sumImaginary * sumImaginary) * microphoneScale * microphoneScale;
|
||||
}
|
||||
output[pointIndex] = accumulatedPower / frameCount;
|
||||
if ((pointIndex + 1) % pointBatchSize === 0) await yieldToEventLoop();
|
||||
if ((pointIndex + 1) % pointBatchSize === 0 && pointIndex + 1 < pointCount) {
|
||||
await cooperativeYield();
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -255,32 +273,45 @@ export function createDasAnalysisPlan(options) {
|
||||
const pointCount = experiment.scan.columns * experiment.scan.rows;
|
||||
const pointMicrophonePairs = pointCount * microphoneCount;
|
||||
const distanceMatrixBytes = pointMicrophonePairs * Float32Array.BYTES_PER_ELEMENT;
|
||||
const stftWorkspaceBytes = fftSize * Float64Array.BYTES_PER_ELEMENT * 3;
|
||||
const calibrationBytes =
|
||||
selectedBins.length * microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
|
||||
const retainedBaseBytes =
|
||||
distanceMatrixBytes +
|
||||
pointCount * Float32Array.BYTES_PER_ELEMENT +
|
||||
microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
|
||||
microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2 +
|
||||
stftWorkspaceBytes;
|
||||
const retainedNonSteeringBytes = retainedBaseBytes + calibrationBytes;
|
||||
if (
|
||||
!Number.isSafeInteger(pointMicrophonePairs) ||
|
||||
!Number.isSafeInteger(retainedBaseBytes) ||
|
||||
retainedBaseBytes > maximumPlanBytes
|
||||
!Number.isSafeInteger(calibrationBytes) ||
|
||||
!Number.isSafeInteger(retainedNonSteeringBytes) ||
|
||||
retainedNonSteeringBytes > maximumPlanBytes
|
||||
) {
|
||||
throw new RangeError(
|
||||
"CPU analysis plan requires " + retainedBaseBytes.toLocaleString() +
|
||||
" base bytes, above maximumPlanBytes " + maximumPlanBytes.toLocaleString() + ".",
|
||||
"CPU analysis plan requires " + retainedNonSteeringBytes.toLocaleString() +
|
||||
" non-steering bytes including " + calibrationBytes.toLocaleString() +
|
||||
" calibration bytes, above maximumPlanBytes " +
|
||||
maximumPlanBytes.toLocaleString() + ".",
|
||||
);
|
||||
}
|
||||
const distances = createDistanceMatrix(arrayDefinition, experiment.scan);
|
||||
const binMap = new Float32Array(pointCount);
|
||||
const steeringReal = new Float64Array(microphoneCount);
|
||||
const steeringImaginary = new Float64Array(microphoneCount);
|
||||
const stftWorkspace = createStftWorkspace(fftSize);
|
||||
const steeringBytesPerBin =
|
||||
pointCount * microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
|
||||
const requestedSteeringCacheBytes = steeringBytesPerBin * selectedBins.length;
|
||||
const steeringCacheEnabled =
|
||||
Number.isSafeInteger(requestedSteeringCacheBytes) &&
|
||||
requestedSteeringCacheBytes > 0 &&
|
||||
requestedSteeringCacheBytes <= maximumSteeringCacheBytes &&
|
||||
retainedBaseBytes + requestedSteeringCacheBytes <= maximumPlanBytes;
|
||||
const steeringCacheBinCapacity = Math.min(
|
||||
selectedBins.length,
|
||||
Math.floor(maximumSteeringCacheBytes / steeringBytesPerBin),
|
||||
Math.floor((maximumPlanBytes - retainedNonSteeringBytes) / steeringBytesPerBin),
|
||||
);
|
||||
const steeringCacheBytes = steeringCacheBinCapacity * steeringBytesPerBin;
|
||||
const steeringCacheEnabled = steeringCacheBinCapacity > 0;
|
||||
const steeringCachedBins = new Set(selectedBins.slice(0, steeringCacheBinCapacity));
|
||||
|
||||
return {
|
||||
kind: DAS_ANALYSIS_PLAN_KIND,
|
||||
@@ -297,13 +328,20 @@ export function createDasAnalysisPlan(options) {
|
||||
calibrations: new Map(),
|
||||
steeringMatrices: new Map(),
|
||||
steeringCacheEnabled,
|
||||
steeringCacheBinCapacity,
|
||||
steeringCachedBins,
|
||||
steeringBytesPerBin,
|
||||
steeringCacheBytes,
|
||||
requestedSteeringCacheBytes,
|
||||
retainedBaseBytes,
|
||||
calibrationBytes,
|
||||
retainedNonSteeringBytes,
|
||||
maximumSteeringCacheBytes,
|
||||
maximumPlanBytes,
|
||||
binMap,
|
||||
steeringReal,
|
||||
steeringImaginary,
|
||||
stftWorkspace,
|
||||
busy: false,
|
||||
statistics: {
|
||||
analysisAttempts: 0,
|
||||
@@ -315,8 +353,20 @@ export function createDasAnalysisPlan(options) {
|
||||
calibrationCacheHits: 0,
|
||||
steeringMatrixBuilds: 0,
|
||||
steeringCacheHits: 0,
|
||||
uncachedSteeringUses: 0,
|
||||
steeringPointMicrophonePairs: 0,
|
||||
uncachedSteeringPointMicrophonePairs: 0,
|
||||
avoidedSteeringPointMicrophonePairs: 0,
|
||||
stftWindowBuilds: 1,
|
||||
stftWorkspaceAllocations: 1,
|
||||
stftWorkspaceUses: 0,
|
||||
avoidedStftWindowBuilds: 0,
|
||||
avoidedStftWorkspaceAllocations: 0,
|
||||
stftTransforms: 0,
|
||||
fftButterflies: 0,
|
||||
beamformingComplexMacs: 0,
|
||||
binMapAllocations: 1,
|
||||
steeringWorkspaceAllocations: 1,
|
||||
binMapUses: 0,
|
||||
avoidedBinMapAllocations: 0,
|
||||
retainedBytes: retainedBaseBytes,
|
||||
@@ -350,6 +400,7 @@ function planCalibration(plan, bin) {
|
||||
plan.sampleRate,
|
||||
plan.experiment.stft.fftSize,
|
||||
plan.experiment.beamforming.compensateFrequencyResponse !== false,
|
||||
plan.experiment.beamforming.maxCompensationDb,
|
||||
);
|
||||
plan.calibrations.set(bin, calibration);
|
||||
plan.statistics.calibrationBuilds += 1;
|
||||
@@ -358,7 +409,12 @@ function planCalibration(plan, bin) {
|
||||
}
|
||||
|
||||
async function planSteeringMatrix(plan, bin, calibration, signal, pointBatchSize) {
|
||||
if (!plan.steeringCacheEnabled) return undefined;
|
||||
if (!plan.steeringCachedBins.has(bin)) {
|
||||
plan.statistics.uncachedSteeringUses += 1;
|
||||
plan.statistics.uncachedSteeringPointMicrophonePairs +=
|
||||
plan.pointCount * plan.microphoneCount;
|
||||
return undefined;
|
||||
}
|
||||
let matrix = plan.steeringMatrices.get(bin);
|
||||
if (matrix) {
|
||||
plan.statistics.steeringCacheHits += 1;
|
||||
@@ -393,12 +449,31 @@ function analysisCacheSummary(plan) {
|
||||
avoidedDistanceEvaluations: plan.statistics.avoidedDistanceEvaluations,
|
||||
calibrationBuilds: plan.statistics.calibrationBuilds,
|
||||
calibrationCacheHits: plan.statistics.calibrationCacheHits,
|
||||
calibrationBytes: plan.calibrationBytes,
|
||||
steeringCacheEnabled: plan.steeringCacheEnabled,
|
||||
steeringCacheBinCapacity: plan.steeringCacheBinCapacity,
|
||||
steeringCachedBinCount: plan.steeringMatrices.size,
|
||||
steeringUncachedBinCount: plan.selectedBins.length - plan.steeringCacheBinCapacity,
|
||||
steeringCacheBytes: plan.steeringCacheBytes,
|
||||
requestedSteeringCacheBytes: plan.requestedSteeringCacheBytes,
|
||||
steeringMatrixBuilds: plan.statistics.steeringMatrixBuilds,
|
||||
steeringCacheHits: plan.statistics.steeringCacheHits,
|
||||
uncachedSteeringUses: plan.statistics.uncachedSteeringUses,
|
||||
steeringPointMicrophonePairs: plan.statistics.steeringPointMicrophonePairs,
|
||||
uncachedSteeringPointMicrophonePairs:
|
||||
plan.statistics.uncachedSteeringPointMicrophonePairs,
|
||||
avoidedSteeringPointMicrophonePairs:
|
||||
plan.statistics.avoidedSteeringPointMicrophonePairs,
|
||||
stftWindowBuilds: plan.statistics.stftWindowBuilds,
|
||||
stftWorkspaceAllocations: plan.statistics.stftWorkspaceAllocations,
|
||||
stftWorkspaceUses: plan.statistics.stftWorkspaceUses,
|
||||
avoidedStftWindowBuilds: plan.statistics.avoidedStftWindowBuilds,
|
||||
avoidedStftWorkspaceAllocations: plan.statistics.avoidedStftWorkspaceAllocations,
|
||||
stftTransforms: plan.statistics.stftTransforms,
|
||||
fftButterflies: plan.statistics.fftButterflies,
|
||||
beamformingComplexMacs: plan.statistics.beamformingComplexMacs,
|
||||
binMapAllocations: plan.statistics.binMapAllocations,
|
||||
steeringWorkspaceAllocations: plan.statistics.steeringWorkspaceAllocations,
|
||||
avoidedBinMapAllocations: plan.statistics.avoidedBinMapAllocations,
|
||||
retainedBytes: plan.statistics.retainedBytes,
|
||||
};
|
||||
@@ -419,12 +494,13 @@ export function estimateDasWorkload(options) {
|
||||
const outputBytes = bandCount * pointCount * Float32Array.BYTES_PER_ELEMENT * 2;
|
||||
const binMapBytes = pointCount * Float32Array.BYTES_PER_ELEMENT;
|
||||
const spectrumBytesPerBin = frameCount * microphoneCount * Float32Array.BYTES_PER_ELEMENT * 2;
|
||||
const fftWorkspaceBytes = fftSize * Float64Array.BYTES_PER_ELEMENT * 2;
|
||||
const steeringCacheBytes = analysisPlan?.steeringCacheEnabled
|
||||
? analysisPlan.requestedSteeringCacheBytes
|
||||
: 0;
|
||||
const fftWorkspaceBytes = fftSize * Float64Array.BYTES_PER_ELEMENT * 3;
|
||||
const steeringCacheBytes = analysisPlan?.steeringCacheBytes ?? 0;
|
||||
const calibrationBytes = analysisPlan?.calibrationBytes ??
|
||||
microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
|
||||
const baseWorkingBytes =
|
||||
distanceBytes + outputBytes + binMapBytes + fftWorkspaceBytes + steeringCacheBytes;
|
||||
distanceBytes + outputBytes + binMapBytes + fftWorkspaceBytes +
|
||||
steeringCacheBytes + calibrationBytes;
|
||||
const complexMacCount = pointCount * microphoneCount * frameCount * selectedBins.length;
|
||||
return {
|
||||
bandBins,
|
||||
@@ -434,7 +510,10 @@ export function estimateDasWorkload(options) {
|
||||
pointCount,
|
||||
distanceBytes,
|
||||
outputBytes,
|
||||
fftWorkspaceBytes,
|
||||
steeringCacheBytes,
|
||||
calibrationBytes,
|
||||
steeringCacheBinCapacity: analysisPlan?.steeringCacheBinCapacity ?? 0,
|
||||
spectrumBytesPerBin,
|
||||
baseWorkingBytes,
|
||||
complexMacCount,
|
||||
@@ -537,16 +616,21 @@ async function computeDasMapInternal(options, analysisPlan) {
|
||||
|
||||
let completedBins = 0;
|
||||
for (let batchStart = 0; batchStart < workload.selectedBins.length; batchStart += batchSize) {
|
||||
throwIfAborted(signal);
|
||||
await yieldToEventLoop();
|
||||
throwIfAborted(signal);
|
||||
const batchBins = workload.selectedBins.slice(batchStart, batchStart + batchSize);
|
||||
if (analysisPlan) {
|
||||
analysisPlan.statistics.stftWorkspaceUses += 1;
|
||||
const repeatedUses = Math.max(0, analysisPlan.statistics.stftWorkspaceUses - 1);
|
||||
analysisPlan.statistics.avoidedStftWindowBuilds = repeatedUses;
|
||||
analysisPlan.statistics.avoidedStftWorkspaceAllocations = repeatedUses;
|
||||
}
|
||||
const stft = await extractStftBins(channels, {
|
||||
fftSize: experiment.stft.fftSize,
|
||||
hopSize: experiment.stft.hopSize,
|
||||
bins: batchBins,
|
||||
signal,
|
||||
transformBatchSize: stftTransformBatchSize,
|
||||
workspace: analysisPlan?.stftWorkspace,
|
||||
onProgress: ({ completedTransforms, totalTransforms }) =>
|
||||
onProgress?.({
|
||||
stage: "stft",
|
||||
@@ -558,7 +642,15 @@ async function computeDasMapInternal(options, analysisPlan) {
|
||||
spectrumBatchCount,
|
||||
}),
|
||||
});
|
||||
await yieldToEventLoop();
|
||||
if (analysisPlan) {
|
||||
const completedTransformCount = stft.frameCount * stft.microphoneCount;
|
||||
analysisPlan.statistics.stftTransforms += completedTransformCount;
|
||||
analysisPlan.statistics.fftButterflies +=
|
||||
completedTransformCount *
|
||||
(experiment.stft.fftSize / 2) *
|
||||
Math.log2(experiment.stft.fftSize);
|
||||
}
|
||||
await cooperativeYield();
|
||||
throwIfAborted(signal);
|
||||
|
||||
for (const bin of batchBins) {
|
||||
@@ -585,6 +677,7 @@ async function computeDasMapInternal(options, analysisPlan) {
|
||||
speedOfSound: scene.speedOfSound,
|
||||
compensateFrequencyResponse:
|
||||
experiment.beamforming.compensateFrequencyResponse !== false,
|
||||
maxCompensationDb: experiment.beamforming.maxCompensationDb,
|
||||
calibration,
|
||||
steeringMatrix,
|
||||
output: analysisPlan?.binMap,
|
||||
@@ -593,6 +686,10 @@ async function computeDasMapInternal(options, analysisPlan) {
|
||||
signal,
|
||||
pointBatchSize,
|
||||
});
|
||||
if (analysisPlan) {
|
||||
analysisPlan.statistics.beamformingComplexMacs +=
|
||||
workload.pointCount * workload.microphoneCount * stft.frameCount;
|
||||
}
|
||||
for (const bandIndex of bandIndexesByBin.get(bin)) {
|
||||
const rawMap = rawMaps[bandIndex];
|
||||
for (let pointIndex = 0; pointIndex < workload.pointCount; pointIndex += 1) {
|
||||
|
||||
Reference in New Issue
Block a user