Tests : comparaison du moteur embarqué aux références de gnubg

tests/test_engine_reference.sh lance tests/reference/compare.py sur les
32 positions aux niveaux 0 et 2 plis. Tolérance de 0,002 mesurée et
justifiée dans tests/reference/README.md : écart maximal de 9,5e-4, avec
ou sans -ffast-math ; la cause est la sigmoïde approchée (_mm_rcp_ps) de
gnubg de bureau en SSE2, qu'un build SSE2 du moteur reproduit au bit près.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-02 14:18:10 +01:00
co-authored by Claude Opus 5.5
parent ae166d51e5
commit 83cb1a6f83
3 changed files with 276 additions and 6 deletions
+86 -6
View File
@@ -12,13 +12,15 @@ d'un fichier de résultat.
| `positions.json` | Positions d'entrée, versionnées, écrites à la main |
| `results/<id>.json` | Résultats de gnubg, un fichier par position, générés |
| `generate.py` | Script exécuté dans gnubg qui produit `results/` |
| `compare.py` | Compare le moteur embarqué à `results/` (dernière section) |
Commandes :
```
make reference-image # construit l'image Docker du gnubg de référence
make reference # régénère results/ (une vingtaine de secondes)
make test # vérifie la cohérence (tests/test_reference.sh)
make test # vérifie la cohérence (tests/test_reference.sh) et
# compare le moteur embarqué (tests/test_engine_reference.sh)
```
`make reference` est déterministe : deux exécutions donnent des fichiers
@@ -222,8 +224,86 @@ Sortie de `gnubg.cfevaluate` (`GeneralCubeDecisionE` puis
## Comparaison avec le moteur embarqué
Une concordance au bit près n'est pas attendue (ADR 0005) : `-ffast-math` et
le jeu d'instructions (SSE2 ici, NEON sur téléphone, C générique en
WebAssembly) changent les derniers chiffres. Les tests de concordance devront
donc comparer les nombres avec une tolérance, et accepter qu'entre deux coups
d'équités presque égales l'ordre s'inverse.
`make test` compare le moteur embarqué à ces résultats
(`tests/test_engine_reference.sh`). Pour chaque position et chaque niveau, le
comparateur `compare.py` (Python 3, dans l'image Docker de la chaîne C) lance
```
build/engine-cli --ply N reference POSITION_ID MATCH_ID [DÉS]
```
qui produit un objet de même structure que `plies["<n>"]` : `evaluation`,
puis `moves` (avec les dés) ou `cube` (sans). Le classement des coups suit la
méthode de `hint` décrite plus haut (`engine_best_moves` dans
`engine/engine.c`). Pour voir tous les nombres comparés :
`BUILD_C_IMAGE=ustagammon/build-c:2 tests/test_engine_reference.sh --details`.
Une concordance au bit près n'est pas attendue (ADR 0005) : le jeu
d'instructions (SSE2 pour gnubg de bureau, C générique pour le moteur
embarqué sur PC et en WebAssembly, NEON sur téléphone) change les derniers
chiffres. Les nombres sont donc comparés avec une tolérance.
### Critères
Pour chaque position et chaque niveau :
- même nombre de coups légaux ;
- même meilleur coup, sauf si gnubg lui-même sépare ses deux premiers coups
de moins que la tolérance : c'est alors une égalité, signalée par une
remarque dans le rapport, sans échec ;
- même pli réel du meilleur coup ;
- pour chaque coup présent dans les deux classements (10 premiers), équité
et cinq probabilités à la tolérance près ;
- évaluation de la position (cinq probabilités et équité) à la tolérance
près ;
- même décision de videau (`decision` et `decision_text`), et les quatre
équités du videau à la tolérance près.
Le rapport donne, pour chaque écart hors critères, la position et le niveau,
le champ, la valeur attendue (gnubg), la valeur obtenue (moteur) et l'écart ;
il donne toujours l'écart maximal observé.
### Tolérance : 0,002
Une même tolérance absolue s'applique aux probabilités et aux équités
(équités ramenées à un videau de 1). Elle vient de mesures faites le
2026-11-02 sur les 32 positions aux deux niveaux (2 598 nombres comparés) :
| Moteur comparé à gnubg de bureau | 99 % des écarts sous | Écart maximal |
| --- | --- | --- |
| Moteur embarqué tel que compilé (C générique, `-O3 -ffast-math`) | 1,6e-4 | 9,5e-4 |
| Le même sans `-ffast-math` | 1,6e-4 | 9,5e-4 |
| Le même compilé en SSE2 comme gnubg de bureau (essai, non retenu) | 0 | 0 (float32 identiques) |
Ce que ces mesures montrent :
- **`-ffast-math` n'est pas en cause côté moteur embarqué** : avec ou sans,
le moteur en C générique donne les mêmes classements, et ses nombres
diffèrent entre eux de 1,1e-6 au plus.
- **La cause est le code SIMD de gnubg de bureau.** En SSE2 avec
`-ffast-math`, la sigmoïde du réseau de neurones (`sigmoid_positive_ps`
dans `lib/neuralnetsse.c`) calcule `1/x` avec l'instruction approchée
`_mm_rcp_ps` (environ 12 bits de précision), alors que la version en C
générique (`sigmoid` dans `lib/sigmoid.h`) fait une vraie division. L'essai
le prouve : le même moteur compilé en SSE2 (`-DUSE_SIMD_INSTRUCTIONS
-DHAVE_SSE -DUSE_SSE2`) redonne les références au bit près, sur les 2 604
nombres comparés (classements identiques, donc un peu plus de coups
communs).
- **L'écart reste en général de l'ordre de 1e-4**, sauf un cas :
`opening-64-match` à 2 plis, coup `8/2 6/2`, écart de 9,5e-4 alors que les
autres coups de la même position restent sous 1e-5. Une recherche à 2 plis
fait des choix discrets (meilleure réponse pour chaque jet, élagage) : quand
deux choix sont presque à égalité, un écart de 1e-4 peut en faire basculer
un, et l'écart final grandit d'autant.
La tolérance de 0,002 vaut environ deux fois l'écart maximal mesuré, pour
absorber d'autres bascules de ce genre sans masquer un vrai défaut : elle
reste quinze fois plus petite que le seuil à partir duquel gnubg qualifie un
coup de « douteux » (0,03) et correspond à deux unités du dernier chiffre
qu'affiche gnubg (trois décimales). Les vrais défauts (mauvais niveau,
mauvais contexte de videau, mauvaise conversion d'équité) donnent des écarts
d'un ou plusieurs centièmes.
Sur téléphone (NEON) les écarts seront à mesurer de la même façon : avec
`-ffast-math`, gnubg y remplace aussi la division par une approximation
(`vrecpeq_f32` et une itération de Newton), différente de celle de SSE2.