Los mismos bits en cualquier máquina
Un filtro de ponderación diseñado por phonometry es el mismo filtro, byte a
byte, en cualquier máquina que comparta biblioteca C: cualquier CPU, con o
sin AVX-512; cualquier juego de kernels BLAS; cualquier número de hilos;
cualquier semilla de hash. Entre bibliotecas C todavía no: Linux, macOS y
Windows diseñan cada uno sus propios bytes para el mismo filtro, porque un
puñado de trascendentes sigue saliendo del math de la plataforma una vez
por diseño, y esta página las nombra y mide lo que hacen. Dentro de una
plataforma, los coeficientes que diseña un portátil son los que diseña un
clúster, así que un veredicto de conformidad, una figura publicada o un
filtro embarcado en una cadena de medida es un artefacto reproducible y no
el relato de una máquina sobre sí misma.
Esa frase no es el comportamiento normal del Python científico, y esta página registra lo que costó, con los números.
Por qué la coma flotante deriva entre hosts
Sección titulada «Por qué la coma flotante deriva entre hosts»IEEE 754 fija cada operación individual: dados dos dobles, su suma, resta, producto, cociente y raíz cuadrada son el mismo patrón de bits en toda máquina conforme. Todo lo que va más allá de una operación es por donde se escapa la reproducibilidad, por tres puertas distintas.
Las reducciones pueden reasociarse. Una suma de muchos términos no tiene orden definido, y un BLAS elige el que conviene a los registros vectoriales que encuentra en tiempo de ejecución. phonometry midió lo que eso cuesta sobre su propio ajuste de ponderaciones: dos juegos de kernels de OpenBLAS coincidieron en todas las decisiones de aceptación de la búsqueda de Levenberg-Marquardt durante treinta y nueve pasos consecutivos mientras los costes que comparaban derivaban de 3 × 10⁻¹³ a 1 × 10⁻⁴ de separación, y las dos ejecuciones aterrizaron después en filtros distintos: 5 × 10⁻⁵ de separación en el coeficiente principal, 0,002 dB en respuesta, unos visibles 0,0225 pt en una curva dibujada. Un optimizador en un valle plano amplifica lo que haga el último bit.
Las trascendentes vectorizadas se despachan. numpy elige los kernels de
log, exp, tan, power, arctan y sin según lo que ofrece la CPU, y
los kernels AVX-512 no coinciden hasta el último bit con los que corre una
máquina sin AVX-512. Medido bajo Intel SDE emulando Skylake-X contra el mismo
host en nativo: cada una de esas seis devuelve un digest distinto sobre las
mismas entradas. La fuga llega más lejos que los sitios de llamada obvios:
numpy.geomspace es 10 ** linspace(...), así que la propia rejilla de
frecuencias sobre la que se evalúa cada residuo del ajuste salía distinta
bajo AVX-512, y todos los pasos aguas abajo la heredaban.
La libm de la plataforma es una elección de plataforma. math.log es el
log que traiga la biblioteca C. El de glibc y el de Apple son ambos exactos
hasta cerca de media ulp, lo que significa que en un puñado de entradas
redondean legítimamente a vecinos opuestos, así que un bucle de diseño
construido sobre math.log devolvía filtros distintos en macOS y en Linux
sin nada en ningún sitio que lo dijera. Lo mismo vale para cualquier otra
trascendente que traiga la biblioteca C, y por eso la evidencia de abajo
sigue encontrando tres juegos de bytes.
Qué fija phonometry
Sección titulada «Qué fija phonometry»El camino de diseño determinista responde a cada puerta en su terreno, en
filters._weighting_design y filters._pinned_log:
- Sus propias reducciones. Cada suma que el ajuste compara o con la que
resuelve se pliega por pares en una forma que fija el código, y las
ecuaciones normales se resuelven por eliminación gaussiana escrita a mano,
el mismo algoritmo que corre el
dgesvde LAPACK con el orden ya fuera de la elección de una biblioteca. El plegado por pares es además el orden más exacto: un redondeo O(ε log n) contra el O(ε n) de un acumulador corrido, así que no se sacrificó nada. - Aritmética real donde la compleja se despacha. El producto complejo y el módulo complejo fusionan operaciones de forma distinta por CPU, así que los dos puntos donde el camino entregaba a numpy una expresión compleja están escritos en aritmética real.
- Trascendentes fijadas. Fuera de la iteración, cada trascendente se
toma de la biblioteca C una vez por diseño. Esas pocas llamadas, un
exp, unatan, unaatan, unapow, un par delog10y un par desin, son el único punto por el que la libm de la plataforma sigue entrando en un diseño, y bastan para separar las plataformas: la evidencia de abajo trae los tres digests. Deletrearlas en numpy fijado como está deletreado el logaritmo es el paso que falta para un único digest en todas partes. Dentro de la iteración, el logaritmo corre tres cuartos de millón de veces por diseño, y el bucle que lo fijaba costó un factor cuatro; ahora es el propio algoritmo delogde glibc, el de tablas, deletreado en operaciones numpy que IEEE 754 fija exactamente, aritmética simple, trabajo de bits enteros y consultas de tabla, con los multiply-add fusionados del original reproducidos por transformaciones exactas libres de error. Sus bits son suyos en toda máquina, que es más fuerte que llamar a la libm de nadie. - La rejilla deletreada. La rejilla geométrica de frecuencias se genera término a término con los extremos fijados, así que el ajuste evalúa las mismas frecuencias en todas partes.
La evidencia
Sección titulada «La evidencia»Cada afirmación de arriba está medida, y la mayoría las re-miden tests que corren en CI:
- 91.658.333 entradas, cero excepciones. Cada valor distinto que el
corpus de diseño entero pasa a su logaritmo se capturó y se comparó bit a
bit contra el
logde glibc: idéntico en todos, más alrededor de cien millones de sorteos adversariales sobre el rango completo de exponentes, la banda cercana a uno, los números subnormales (los «denormalizados» de la literatura clásica) y los bordes de las ramas. Una búsqueda adversarial encontró también el límite honesto: en torno a una entrada de cada treinta millones, en una banda estrecha, cuyo logaritmo verdadero queda a media ulp de ambos vecinos y donde las dos implementaciones se separan en el último bit. Ninguna puede alcanzar un diseño, porque en el camino de diseño la rutina fijada es la definición. - El corpus es byte-idéntico. Los diseños de siete curvas a diecinueve frecuencias de muestreo, 133 en total, dan el mismo SHA-256 único antes y después de vectorizar el logaritmo: ningún coeficiente, figura ni valor de conformidad publicado se movió.
- Digest a digest entre entornos. Los diseños son bit-idénticos entre este host y el mismo host bajo emulación AVX-512, bajo cada juego de kernels de OpenBLAS seleccionable, cada número de hilos y cada semilla de hash; las comparaciones entorno contra entorno están fijadas como tests.
- Cada plataforma sus bytes, medidos. Con el logaritmo fijado, un diseño
ya no depende de qué
logtraiga la biblioteca C, pero las llamadas de una vez por diseño sí, y bastan: la ponderación A a 48 kHz da991833ff389afe91en Linux (glibc, x86-64),4efa1818bd80e23aen macOS (arm64) y79852a57e60961c8en Windows (x86-64), el mismo valor con Python 3.13 y 3.14 en cada una. Los tres digests están fijados como test en la matriz de CI, así que una plataforma que derive de su propio valor pone el CI en rojo. Los tests de respuesta que corren en esas tres plataformas sujetan el diseño A a 0,013 dB de su prototipo analógico en toda su banda de ajuste, lo que acota en cuánto pueden diferir en respuesta los tres juegos de bytes.
Compruébalo tú
Sección titulada «Compruébalo tú»Dos diseños son el mismo diseño, y los bytes tienen nombre:
import hashlibfrom phonometry import filters
una = filters.WeightingFilter(48000, "A").sosotra = filters.WeightingFilter(48000, "A").sosprint(una.tobytes() == otra.tobytes()) # Truedigest = hashlib.sha256(una.tobytes()).hexdigest()print(digest[:16]) # 991833ff389afe91 en LinuxLa segunda línea es la clave: esos dieciséis dígitos hexadecimales, la cabeza del SHA-256, no son «lo que salió en mi máquina», son el digest de la ponderación A a 48 kHz de esta versión en cualquier máquina Linux con glibc en x86-64, la pata que fija la matriz de CI, y la evidencia de arriba da los valores que esperar en macOS y Windows. Si una versión futura mueve un coeficiente a propósito, el digest se mueve con ella y el cambio es un suceso documentado, nunca una propiedad de tu hardware.
Alcance
Sección titulada «Alcance»La garantía cubre el camino de diseño determinista de ponderaciones de punta a punta dentro de una plataforma, y el procesamiento por bloques compone con ella: una cascada con estado alimentada bloque a bloque es bit-idéntica a una sola pasada sobre el registro entero, así que el streaming no gasta la garantía. Las funciones de análisis generales construidas sobre numpy y SciPy en su forma corriente siguen siendo reproducibles en una misma máquina pero pueden diferir en los últimos bits entre CPUs, que es la condición normal del Python científico; el tratamiento determinista se aplica donde importa la identidad de un filtro.
El relato de ingeniería completo vive con el código, en los docstrings de
módulo de
filters/_weighting_design.py
y
filters/_pinned_log.py,
y los tests que lo sostienen están en
tests/filters/test_weighting_design.py
y
tests/filters/test_pinned_log.py.
Véase también
Sección titulada «Véase también»- Informe de conformidad: contra qué se comprueban los números; esta página es la razón de que las comprobaciones lean igual en todas partes.
- Erratas de las fuentes publicadas: la otra mitad de la historia de la evidencia: donde lo que está mal es el valor esperado impreso.
- Ponderaciones en frecuencia: la guía de las curvas que realiza el diseño determinista.
- Procesamiento por bloques: la identidad de streaming en la que se apoya la sección de alcance.