Spaces:
Running
Running
File size: 88,962 Bytes
7b4b77c e4d38df 7b4b77c e4d38df 7b4b77c e4d38df 1f08b54 e4d38df 1f08b54 7b4b77c |
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 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 |
<!DOCTYPE html>
<html>
<head>
<script src="distill.bundle.js" type="module" fetchpriority="high" blocking></script>
<script src="main.bundle.js" type="module" fetchpriority="low" defer></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf8">
<base target="_blank">
<title>Le FAT5 : Flash Attention T5</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<d-front-matter>
<script id='distill-front-matter' type="text/json">{
"title": "Le FAT5 : Flash Attention T5",
"description": "",
"published": "May 28, 2024",
"authors": [
{
"author":"Boris ALBAR",
"authorURL":"https://github.com/b-albar",
"affiliation": [{"name": "CATIE", "url": "https://catie.fr"}]
},
{
"author":"Loïck BOURDOIS",
"authorURL":"https://github.com/lbourdois",
"affiliation": [{"name": "CATIE", "url": "https://catie.fr"}]
}
],
"color": "#9CA3AF",
"katex": {
"delimiters": [
{"left": "$$", "right": "$$", "display": false}
]
}
}
</script>
</d-front-matter>
<d-title>
<h1 class="l-page" style="text-align: center;">Le FAT5 : Flash Attention T5</h1>
<p><img src="./assets/FAT5_dark.gif" alt="FAT5" width="100%"></p>
</d-title>
<d-article>
<d-contents>
</d-contents>
<div class="note">Pour une meilleure expérience, nous déconseillons la lecture sur téléphone portable</div>
<br>
<h2 id="motivation">Motivation</h2>
<p class="width_125">
Alors que beaucoup d’efforts ont été consacrés à l’optimisation de <i>transformer</i> de type décodeur, abandonnant ainsi l’encodeur,
nous pensons qu’il est essentiel de maintenir une architecture encodeur-décodeur.<br>
En effet, cette architecture qui présente des performances intéressantes pour l’instruction tuning <d-cite bibtex-key="chia2023instructeval"></d-cite>,
est propice à la distillation <d-cite bibtex-key="hsieh2023distilling"></d-cite> et semble supérieure aux modèles décodeur lorsqu’elle est finetunée <d-cite bibtex-key="fu2024tiny"></d-cite>.
Il a aussi été montré que les modèles encodeur-décodeur entraînés avec une modélisation du langage masqué obtiennent une meilleure performance zéro-shot
après un finetuning multitâche par rapport à un modèle décodeur <d-cite bibtex-key="wang2022languagemodelarchitecturepretraining"></d-cite>.<br>
Au-delà du NLP sur lequel nous nous concentrons dans cet article de blog, l’architecture encodeur-décodeur est très utilisée dans d’autres domaines
comme l’audio ou les séries temporelles par exemple. L'encodeur d'une telle architecture est également utilisé dans des modèles de diffusion.<br>
Dans cette logique, nous avons décidé de nous concentrer sur le T5 <d-cite bibtex-key="JMLR:v21:20-074"></d-cite>.<br><br>
Dans cet article, sont détaillons les optimisations que nous avons mises en place afin de pré-entraîner de manière efficiente un T5 de 147M de paramètres en français en un temps raisonnable (1 461 H pour 419Mds de <i>tokens</i>) et avec des moyens limités (1 unique A100 ; soit un budget de calcul d'environ 2 200 euros).
Pour ce faire, nous avons conçu des noyaux CUDA/Triton afin de rendre la Flash Attention compatible avec T5 et de fournir une inférence linéaire, étendant ainsi la taille du contexte qui peut être prise en compte par le modèle.<br><br>
<strong>Le code de pré-entrainement est disponible sur notre répertoire <a class="link" href="https://github.com/catie-aq/flashT5">GitHub</a> sous licence Apache-2.0. et les poids sur notre compte <a class="link" href="https://hf.co/CATIE-AQ">Hugging Face</a>.</strong>
<p class="width_125"><br><br><br></p>
<h2 id="vue-d-ensemble-de-notre-travail">Vue d’ensemble de notre travail</h2>
<p class="width_125">Nous avons donc choisi de travailler avec un T5 et en pratique avec le nanoT5 <d-cite bibtex-key="nawrot2023nanot5"></d-cite>.<br>
Pour les tâches de prétexte lors du pré-entraînement, nous avons suivi celles d'UL2 <d-cite bibtex-key="tay2023ul2"></d-cite> avec les 7 tâches suivantes :</p>
<pre><code class="lang-py">
denoiser_list=[
{<span class="hljs-string">"mu"</span>: <span class="hljs-number">3.0</span>, <span class="hljs-string">"r"</span>: <span class="hljs-number">0</span>.<span class="hljs-number">15</span>, <span class="hljs-string">"max_spans"</span>: max_token_length, <span class="hljs-string">"prefix"</span>: <span class="hljs-string">"[R]"</span>},
{<span class="hljs-string">"mu"</span>: <span class="hljs-number">8.0</span>, <span class="hljs-string">"r"</span>: <span class="hljs-number">0</span>.<span class="hljs-number">15</span>, <span class="hljs-string">"max_spans"</span>: max_token_length, <span class="hljs-string">"prefix"</span>: <span class="hljs-string">"[R]"</span>},
{<span class="hljs-string">"mu"</span>: <span class="hljs-number">4.0</span>, <span class="hljs-string">"r"</span>: <span class="hljs-number">0</span>.<span class="hljs-number">0</span>, <span class="hljs-string">"max_spans"</span>: <span class="hljs-number">1</span>, <span class="hljs-string">"prefix"</span>: <span class="hljs-string">"[S]"</span>},
{<span class="hljs-string">"mu"</span>: <span class="hljs-number">3.0</span>, <span class="hljs-string">"r"</span>: <span class="hljs-number">0</span>.<span class="hljs-number">5</span>, <span class="hljs-string">"max_spans"</span>: max_token_length, <span class="hljs-string">"prefix"</span>: <span class="hljs-string">"[X]"</span>},
{<span class="hljs-string">"mu"</span>: <span class="hljs-number">8.0</span>, <span class="hljs-string">"r"</span>: <span class="hljs-number">0</span>.<span class="hljs-number">15</span>, <span class="hljs-string">"max_spans"</span>: max_token_length, <span class="hljs-string">"prefix"</span>: <span class="hljs-string">"[X]"</span>},
{<span class="hljs-string">"mu"</span>: <span class="hljs-number">64.0</span>, <span class="hljs-string">"r"</span>: <span class="hljs-number">0</span>.<span class="hljs-number">15</span>, <span class="hljs-string">"max_spans"</span>: max_token_length, <span class="hljs-string">"prefix"</span>: <span class="hljs-string">"[X]"</span>},
{<span class="hljs-string">"mu"</span>: <span class="hljs-number">64.0</span>, <span class="hljs-string">"r"</span>: <span class="hljs-number">0</span>.<span class="hljs-number">5</span>, <span class="hljs-string">"max_spans"</span>: max_token_length, <span class="hljs-string">"prefix"</span>: <span class="hljs-string">"[X]"</span>}]
denoiser_proportions=[<span class="hljs-number">0</span>.<span class="hljs-number">165</span>, <span class="hljs-number">0</span>.<span class="hljs-number">165</span>, <span class="hljs-number">0</span>.<span class="hljs-number">34</span>, <span class="hljs-number">0</span>.0825, <span class="hljs-number">0</span>.0825, <span class="hljs-number">0</span>.0825, <span class="hljs-number">0</span>.0825]
</code></pre>
<p class="width_125">avec <code>mu</code> la taille du n-gram, <code>r</code> le pourcentage de masquage dans le n-gram et <code>prefix</code> le type de tâche de prétexte.
La signification des lettres <code>[R]</code>, <code>[S]</code> et <code>[X]</code> est décrite <a class="link" href="https://huggingface.co/google/ul2#mixture-of-denoisers">ici</a>
et nous vous invitons à consulter notamment l'image explicative <a class="link" href="https://raw.githubusercontent.com/google-research/google-research/master/ul2/figs/mod.png">ici</a>.</p>
<p class="width_125">Pour avoir un entraînement rapide, nous nous sommes orientés sur la Flash Attention <d-cite bibtex-key="dao2022flashattention"></d-cite>.
Cependant, comme celle-ci ne gère pas les biais attentionnels (additifs) du T5, nous avons dû l’étendre en développant un noyau personnalisé.
Plus précisément nous avons développé successivement deux versions de ce noyau.
Dans la première version, au début de notre travail nous transmettions la matrice de biais au noyau.
Dans la version actuelle, nous inspirant du TurboT5 <d-cite bibtex-key="turbot5"></d-cite>,
nous communiquons seulement un tenseur contenant les biais fusionnés afin de matérialiser la matrice de biais à la volée.
Cela permet de passer d’un T5 à mémoire quadratique à un T5 à mémoire linéaire, et par conséquent augmente fortement la taille de contexte que le modèle peut gérer.</p>
<p class="width_125">Notre travail a abouti au pré-entraînement d'un T5 en français de 147M de paramètres : le FAT5 <i>small</i>.<br>
Le jeu de données que nous avons utilisé est composé de la partie en français du corpus CulturaX <d-cite bibtex-key="nguyen2023culturax"></d-cite>
(principale source avec plus de 1258 Go de texte),
de la partie en français de Wikipedia <d-cite bibtex-key="wikidump"></d-cite> (dump 20231101),
de justice_fr (textes de loi française) <d-cite bibtex-key="justice_fr"></d-cite>,
et de 25 000 000 lignes de TheStack <d-cite bibtex-key="Kocetkov2022TheStack"></d-cite>
(l'idée ici est de montrer un peu de code à notre modèle bien que ce ne soit pas notre objectif principal).<br>
Ce modèle a été évalué sur cinq tâches : le résumé de textes, la classification binaire, le question answering, la reconnaissance d’entités nommées et la <i>sentence similarity</i>.</p>
<p class="width_125"><br><br><br></p>
<h2 id="les-d-tails-de-la-recette">Les détails de la recette</h2>
<p class="width_125">Ne disposant que de deux A100 (une de 80Go et une de 40Go), nous avons dû consacrer un peu de temps afin d’implémenter des optimisations permettant d'exploiter au mieux notre matériel.
En effet, avant même d’entraîner un modèle voire de modifier son architecture, il faut s’assurer qu’on optimise l'usage des capacités de calcul de nos GPU.
Plusieurs facteurs peuvent expliquer un entraînement sous-optimal d’un modèle de deep learning :<br>
• Le manque de nourriture (<em>disk-bounded</em> - limitation en disque)<br>
• La bande passante mémoire (<em>memory-bounded</em> - limitation en bande passante mémoire)<br>
• La vitesse de calcul (<em>compute-bounded</em> - limitation en calcul)</p>
<p class="width_125">Idéalement, on aimerait que le modèle soit limité par la vitesse de calcul, c’est-à-dire que le GPU soit utilisé à pleine capacité.
Partant de ce constat, nous avons travaillé sur trois points principaux : <br>
• L’optimisation du disque du GPU <br>
• L’optimisation de la bande passante de la mémoire du GPU <br>
• L’optimisation de l’utilisation des Tensor Cores<br>
</p>
<p class="width_125">Il s’agit donc de points à la fois hardware mais aussi software.</p>
<p></p>
<p class="width_125">Dans la suite de cette section, tout ce que nous avons réalisé/implémenté pour apporter une réponse aux limites rencontrées est disponible dans un encadré en vert. Des notes/commentaires sont trouvables dans un encadré en bleu.
<br><br></p>
<h3 id="optimisation-du-disque-du-gpu">Optimisation du disque du GPU</h3>
<p class="width_125">La limitation en disque intervient soit lors du chargement des données, soit lors des opérations de prétraitement.
Dans les deux cas, cela se matérialise par un problème de lenteur.
<br></p>
<h4 id="acc-s-disques">Accès disques</h4>
<p class="width_125">Si la limitation vient des accès disques, plusieurs solutions sont possibles :</p>
<ul>
<li><p class="width_125"><u>Mettre les données en RAM</u><br>
Cela résout le problème de manière radicale mais suppose que la base de données rentre en RAM, ce qui est loin d’être évident du fait de sa petite taille.</p>
<div class="tip"><p>Ce n’est donc pas la solution que nous avons retenue.</p></div>
</li>
<li><p class="width_125"><u>Mettre les données sur un disque plus rapide et/ou moins utilisé</u><br>
Si vous avez un accès physique à votre serveur de GPU, il est très utile d’intégrer des <a class="link" href="https://fr.wikipedia.org/wiki/NVM_Express">NVMe</a> dans la configuration de celui-ci.</p>
<p class="width_125">Il faut aussi faire attention à ne pas avoir trop de processus de différents entraînements qui tirent sur un même disque.
Il est alors préférable d’avoir plusieurs petits disques plutôt qu’un gros. </p>
<div class="note"><p>Un effet indirect bénéfique est qu’une telle configuration coûte moins cher 😉</p></div>
</li>
</ul>
<ul>
<li class="width_125"><u>Utiliser des formats de fichiers plus efficients notamment au niveau des accès aléatoires</u><br>
Par exemple les fichiers <code>.parquet</code> sont plus efficients que les <code>.csv</code>.
On peut aussi utiliser des formats spécifiquement développés dans ce but comme le <code>.beton</code> de ffcv <d-cite bibtex-key="leclerc2023ffcv"></d-cite>.</li>
<div class="tip"><p>Nous utilisons la bibliothèque Datasets <d-cite bibtex-key="lhoest2021datasets"></d-cite> pour charger et traiter les données que nous utilisons.
Avec cette bibliothèque, les données sont décompressées en local au format <code>Arrow</code>.
De plus, si les données chargées depuis le Hub d’Hugging Face ont été ajoutées avec la fonction <code>push_to_hub()</code>,
alors le jeu de données est par défaut converti au format <code>parquet</code>.</p></div>
</ul>
<ul>
<li class="width_125"><u>Prétokeniser les données</u><br>
L'option la plus efficace est probablement de prétokeniser les données afin d'optimiser les accès.
C'est-à-dire que la tokenisation a lieu dans une étape préalable et non pas à la volée.
</li>
<div class="tip"><p>Nous invitions le lecteur à consulter le code
<a class="link" href="https://github.com/catie-aq/flashT5/blob/main/examples/minipile/pretokenize_minipile.py">suivant</a> qui
illustre la façon dont nous procédons dans notre tutoriel sur le FAT5 appliqué au jeu de données Minipile <d-cite bibtex-key="kaddour2023minipilechallengedataefficientlanguage"></d-cite>.</p></div>
</ul>
<p><br></p>
<h4 id="traitement-des-donn-es">Traitement des données</h4>
<p class="width_125">Si la limitation vient du traitement des données après leur chargement :</p>
<ul>
<li><p class="width_125"><u>Il est possible d’utiliser plusieurs processus pour traiter les données en parallèle</u><br>
Par exemple, le paramètre <code>num_workers</code> du <code>Dataloader</code> de PyTorch <d-cite bibtex-key="paszke2019pytorch"></d-cite>.</p></li>
<div class="tip"><p>Vous pouvez retrouver dans notre code les valeurs que nous utilisons pour ce paramètre pour notre FAT5 <a class="link" href="https://github.com/catie-aq/flashT5/blob/dfe10d498ae0b39082182f807acb509e91992360/configs/fr/fat5-fr-small.yaml#L42">small</a>.</div>
</ul>
<ul>
<li><p class="width_125"><u>Le goulot d’étranglement peut aussi venir du <code>DataCollator</code></u><br>
C'est notamment le cas lorsqu’il y a des tâches complexes à effectuer (masquage d’image ou débruiteurs multiples sur des tâches de NLP).<br>
On pourra alors construire un <code>DataCollator</code> personnalisé pour la tâche.
On appliquera les méthodes traditionnelles pour optimiser la vitesse de celui-ci.
De même, l’emploi de la vectorisation de Numpy permettra de traiter plus rapidement des listes qu'avec des boucles <code>for</code>.
D’une manière générale, Numpy est plus rapide que PyTorch pour ce type de tâches.
On pourra aussi utiliser des méthodes de compilation comme numba <d-cite bibtex-key="10.1145/2833157.2833162"></d-cite> pour Python par exemple.</p></li>
<div class="tip"><p>Nous avons suivi ce principe et développé un <code>DataCollator</code> personnalisé pour notre FAT5 dont vous pouvez consulter le code <a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/data/data_collator_ul2.py">ici</a>.
Il gère les tâches de prétexte UL2 tout en ayant un mécanisme de batch dynamique pour réduire le <em>padding</em> (plus d'informations dans la section suivante).</p></div>
<div class="note"><p>Comme il n’y avait pas d’implémentation du <code>DataCollator</code> d’UL2 disponible en PyTorch jusqu’ici, nous espérons que cela pourra être utile à d’autres travaux.</p></div>
</ul>
<ul>
<li><p class="width_125"><u>Réaliser un <em>padding</em> efficace</u><br>
<p class="width_125">Lorsque l’on travaille avec des séquences, on a naturellement tendance à padder un ensemble de séquences pour pouvoir construire des batchs.
Les <em>tokens</em> de <em>padding</em> engendrent alors des calculs inutiles.<br>
La première chose à faire est de limiter le <em>padding</em> à la séquence de taille maximum et non à une valeur maximale.
C’est la technique du <a class="link" href="https://huggingface.co/learn/nlp-course/chapter3/2?fw=pt#dynamic-padding">padding dynamique</a>.<br>
Avec cette technique, il peut néanmoins rester des <em>tokens</em> de <em>padding</em>. Pour les gérer, il existe deux possibilités :<br>
• soit utiliser une méthode de groupage des données avec des tailles similaires
(par exemple, <a class="link" href="https://huggingface.co/docs/transformers/main_classes/trainer#transformers.TrainingArguments.group_by_length">ce paramètre</a>
dans la bibliothèque Transformers <d-cite bibtex-key="wolf2020huggingfaces"></d-cite> ou encore
<a class="link" href="https://discuss.huggingface.co/t/how-to-implement-trainers-group-by-length-in-pytorch/9232">en récupérant ce sampler</a> pour PyTorch)<br>
• soit concaténer différents exemples dans un DataCollator personnalisé.</p>
<div class="tip"><p>Nous avons opté pour la seconde option et nous renvoyons donc le lecteur à nouveau au <a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/data/data_collator_ul2.py">code</a> de notre DataCollator de mélange de débruiteurs (UL2).</p></div>
<div class="note"><p>Des heuristiques plus optimisées doivent probablement être mises en place.
Nous avons fait un test en proposant une
<a class="link" href="https://github.com/catie-aq/flashT5/blob/dfe10d498ae0b39082182f807acb509e91992360/src/data/data_collator_ul2.py#L45">fonction</a>
dans le <code>DataCollator</code> afin de trier les <code>input_ids</code> et les <code>labels</code> par longueur décroissante.
Néanmoins ceci est plutôt long pour un gain d'empaquetage minime.
Un travail plus conséquent serait à effectuer sur ce point.
</p></div>
</ul>
<p class="width_125"><br><br></p>
<h3 id="optimisation-de-la-bande-passante-de-la-m-moire-du-gpu">Optimisation de la bande passante de la mémoire du GPU</h3>
<p class="width_125">La limitation en bande passante mémoire est plus difficile à traiter.
Une opération limitée par la mémoire est une opération dont le temps global d’exécution est restreint par les accès mémoires.
C’est notamment le cas pour les LLMs particulièrement au niveau de l’inférence.
Le diagnostic peut être posé à partir du <a class="link" href="https://pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html"><em>profiler</em> de PyTorch</a> :</p>
<figure class="width_125">
<img src="https://pytorch.org/tutorials/_static/img/profiler_overview1.png" alt="profiler_overview1.png" width="100%">
<figcaption><center><i>Source : <a class="link" href="https://pytorch.org/tutorials/_static/img/profiler_overview1.png">https://pytorch.org/tutorials/_static/img/profiler_overview1.png</a></i></center></figcaption>
</figure>
<br><br><br>
<p class="width_125">Une autre possibilité pour établir un diagnostic est d’utiliser un simple <code>nvidia-smi</code> :</p>
<figure class="width_125">
<img src="./assets/nvidiasmi.png" alt="nvidiasmi.png" width="100%">
</figure>
<br>
<p class="width_125">Utile pour savoir si un problème est présent mais donne peu d’information sur la nature de ce problème.
C’est pourquoi nous avons une préférence pour le profiler.</p>
<p><br></p>
<h4 id="noyau-cuda">Noyau CUDA</h4>
<p class="width_125">La technique reine pour optimiser la bande passante de la mémoire du GPU est de développer un kernel CUDA fusionnant
dans la SRAM plusieurs opérations limitantes.
Cela peut limiter la copie de larges matrices dans l’HBM pour les recharger immédiatement ensuite dans la SRAM.
C’est désormais une caractéristique courante des <i>transformers</i> décodeurs grâce à la <a class="link" href="https://github.com/Dao-AILab/flash-attention">Flash Attention</a>.</p>
<div class="tip"><p>
Comme la Flash Attention ne gère pas les biais attentionnels (additifs) du T5, nous l’avons étendue en développant un noyau CUDA personnalisé.
Comme évoqué dans l’introduction, c’est en réalité successivement deux versions successives de ce noyau que nous avons implémentées.
Sans pour autant entrer dans les détails des 650 lignes de code de l’implémentation de la première version (consultable
<a class="link" href="https://github.com/Dao-AILab/flash-attention/pull/617">ici</a>),
l’idée générale et simplifiée (pour une passe avant) est la suivante :</p>
<ul>
<li>On charge de l’HBM vers la SRAM la sortie attendue O (output) initialisée avec des 0,
de même que la requête Q (query), la clé K (key), la valeur V (value) et les biais B.</li>
<li>Notre noyau CUDA calcule les étapes suivantes :<br>
• Calcul de la matrice S via le produit matriciel entre Q et la transposée de K<br>
• Calcul de S’ qui est la somme de la matrice S et de la matrice des biais B<br>
• Calcul de P qui est la softmax (cumulative sous le capot) de S’<br>
• Calcul de la sortie O qui est le produit matriciel de P par V<br>
</li>
<li>La sortie O est chargée sur l’HBM et la SRAM est vidée.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/FAT5_dark.gif">
<img alt="FAT5 animation" src="./assets/FAT5.gif" width="100%">
</picture>
</ul>
<br>
<p>Alors que la première version du noyau est générique, la seconde (disponible <a class="link" href="https://github.com/Dao-AILab/flash-attention/pull/956">ici</a>)
est spécifique au fonctionnement de modèles avec encodage positionnel relatif (ce qui est le cas du T5).
L’idée générale et simplifiée (pour une passe avant) est la suivante :</p>
<ul>
<li>Dans l'HBM, nous avons la sortie attendue O (output) initialisée avec des 0,
de même que la requête Q (query), la clé K (key), la valeur V (value).
Cependant nous n'avons pas la matrice de biais B comme précédemment mais le bucket de tenseur T.</li>
<li>On charge de l’HBM vers la SRAM la sortie attendue O (output) initialisée avec des 0,
de même que la requête Q (query), la clé K (key), la valeur V (value) et le bucket de tenseur T.</li>
<li>Notre noyau CUDA calcule les étapes suivantes :<br>
• Calcul de la matrice S via le produit matriciel entre Q et la transposée de K<br>
• Calcul de S’ qui est la somme de la matrice S et d'une matrice remplie avec les éléments de T<br>
• Calcul de P qui est la softmax (cumulative sous le capot) de S’<br>
• Calcul de la sortie O qui est le produit matriciel de P par V<br>
</li>
<li>La sortie O est chargée sur l’HBM et la SRAM est vidée.</li>
</ul>
<p>
De cette façon, alors que la première version la matrice de biais B nécessitait une mémoire quadratique, ici nous nous ramenons
à une mémoire linéaire permettant d'effectuer des inférences sur des dizaines de milliers de <em>tokens</em>.<br>
Pour la conception de cette seconde version, nous nous sommes inspirés du noyau Triton du TurboT5 que nous avons porté en CUDA et étendu au full bf16.
</p>
</div>
<br>
<div class="tip"><p>Notons que les deux versions développées peuvent être utilisées avec plusieurs encodages positionnels.<br>
Nous invitions le lecteur à consulter ce <a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/utils/positional_encoding.py">fichier</a>
contenant des classes compatibles avec la Flash Attention pour le
<a class="link" href="https://github.com/catie-aq/flashT5/blob/dfe10d498ae0b39082182f807acb509e91992360/src/utils/positional_encoding.py#L10">RelativePositionalEncoding</a>
<d-cite bibtex-key="shaw2018selfattention"></d-cite>,
l’<a class="link" href="https://github.com/catie-aq/flashT5/blob/dfe10d498ae0b39082182f807acb509e91992360/src/utils/positional_encoding.py#L113">ALiBiPositionalEncoding</a>
<d-cite bibtex-key="press2022train"></d-cite>,
le <a class="link" href="https://github.com/catie-aq/flashT5/blob/dfe10d498ae0b39082182f807acb509e91992360/src/utils/positional_encoding.py#L205">RotaryPositionalEncoding</a>
<d-cite bibtex-key="su2023roformer"></d-cite> et
<a class="link" href="https://github.com/catie-aq/flashT5/blob/dfe10d498ae0b39082182f807acb509e91992360/src/utils/positional_encoding.py#L341">FIRE</a> <d-cite bibtex-key="li2024functional"></d-cite>.</p></div>
<div class="note"><p>Au moment de la rédaction de ces lignes,
les deux <em>pull request</em> (une pour chaque version du noyau, disponibles <a class="link" href="https://github.com/Dao-AILab/flash-attention/pull/617">ici</a>
et <a class="link" href="https://github.com/Dao-AILab/flash-attention/pull/956">ici</a>) ouvertes sur le répertoire officiel de la Flash Attention n’ont pas été mergées.
Le lecteur devra donc provisoirement recompiler nos patchs personnalisés de la Flash Attention pour pouvoir utiliser nos modèles.<br>
Nous invitons le lecteur à consulter la partie Benchmark plus bas dans l'article pour visualiser l'amélioration apportée par ces deux noyaux.</p></div>
<br>
<div class="note"><p>Bien que nous n’y ayons pas eu recours, il est à noter que certaines libraires contiennent des implémentations d’opérateurs courant fusionnés, par exemple Apex <d-cite bibtex-key="nvidiapex"></d-cite>.</p></div>
<p><br></p>
<h4 id="noyau-triton">Noyau Triton</h4>
<p class="width_125">Triton <d-cite bibtex-key="10.1145/3315508.3329973"></d-cite> est un langage de programmation maintenu qui permet de compiler du code Python efficacement à l’instar de CUDA mais ayant l’avantage d’être (de notre point de vue) plus simple à prendre en main. En effet, contrairement à CUDA, qui nécessite une compréhension approfondie de l’architecture matérielle des GPU, Triton fait abstraction de nombreux détails de bas niveau tels que la coalescence de la mémoire, la gestion de la mémoire partagée et l’ordonnancement au sein des blocs de threads CUDA.</p>
<div class="tip"><p>Une implémentation en Triton de la
<a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/model/ops/flash_attention_v2_bias.py">Flash Attention 2 gérant les biais d’attention</a>
est fournie pour ceux qui ne souhaitent pas recompiler un patch personnalisé pour la Flash Attention.
Pour cela, nous nous sommes appuyés sur le répertoire de FlagAttention <d-cite bibtex-key="flagattention"></d-cite>.
<br>
<br>
En complément de cette implémentation (dont l'usage est facultatif), d’autres parties de l’architecture ont été optimisées à l’aide de noyaux Triton ad hoc, à savoir :
<br>
• la perte d’<a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/model/ops/cross_entropy_loss.py">entropie croisée</a> (et la perte z <d-cite bibtex-key="debrébisson2016zloss"></d-cite>) <br>
• la <a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/model/ops/rms_norm.py">couche de RMSNorm</a> <d-cite bibtex-key="zhang2019root"></d-cite> <br>
<br>
Nous nous sommes notamment inspirés de <a class="link" href="https://github.com/unslothai/unsloth">Unsloth</a> <d-cite bibtex-key="unsloth"></d-cite>.<br>
<br>
Nous invitons le lecteur se référer à la partie Benchmark plus bas dans l'article pour visualiser l'impact de cette optimisation.</div>
<p><br></p>
<h4 id="utiliser-torch-compile-">Utiliser <code>torch.compile</code></h4>
<p class="width_125">Une approche plus simple est de compiler les modèles avec <code>torch.compile</code>.
PyTorch se charge alors de faire les fusions possibles, éventuellement en réordonnant des opérations.
Il s’agit alors de faire la chasse aux cassures dans le graphe de compilation qui sont des retours à un mode d’exécution « eager » impactant
négativement la performance de l’opération.</p>
<div class="note"><p>Voir la <a class="link" href="https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html">documentation</a> officielle pour plus de détails.</p></div>
<p class="width_125">Une autre possibilité consiste à un usage conjoint d’un noyau kernel personnalisé et de <code>torch.compile</code>.
L'implémentation de cette option est grandement simplifiée depuis la
<a class="link" href="https://github.com/pytorch/pytorch/releases/tag/v2.4.0">version 2.4 de PyTorch</a>.</p>
<p class="width_125">Nous invitons le lecteur à se référer à la partie benchmark disponible plus bas dans l’article afin de mesurer les performances mémoire
des différentes techniques décrites.</p>
<p class="width_125"><br><br></p>
<h3 id="optimisation-de-l-utilisation-des-tensor-cores">Optimisation de l’utilisation des Tensor Cores</h3>
<p class="width_125">Les GPU récents possèdent des unités dédiées aux opérations tensorielles : les TensorCore. Les utiliser correctement est essentiel.</p>
<p class="width_125">A nouveau, pour établir un diagnostic, il est conseillé de se référer au profiler de PyTorch qui indique la proportion de TensorCore
utilisé pour chaque noyau CUDA :</p>
<p><figure class="width_125">
<img src="https://pytorch.org/tutorials/_static/img/profiler_kernel_view.png" alt="profiler_kernel_view.png" width="100%">
<figcaption><center><i>Source : <a class="link" href="https://pytorch.org/tutorials/_static/img/profiler_kernel_view.png">https://pytorch.org/tutorials/_static/img/profiler_kernel_view.png</a></i></center></figcaption>
</figure>
<br><br>
<p class="width_125">Concernant les optimisations réalisables :<br></p>
<h4 id="puissances-de-2">Multiple de 8 ou de 64</h4>
<p class="width_125">La première consiste à employer des tailles de tenseurs de certains multiples de 8 ou de 64.
Nous invitons le lecteur à se référer à la documentation de Nvidia,
en particulier cet <a class="link" href="https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html#requirements-tc">article</a>
et cet <a class="link" href="https://developer.nvidia.com/blog/optimizing-gpu-performance-tensor-cores/">article</a> pour déterminer le multiple à sélectionner en fonction de la précision désirée. </p>
<div class="tip"><p>Dans cette logique, nous avons entraîné un <em>tokenizer</em> de taille 32 768 (8**5), suivant <a class="link" href="https://twitter.com/karpathy/status/1621578354024677377">cette observation de KARPATHY</a>.
Il s'agit d'un BPE <d-cite bibtex-key="sennrich2016neuralmachinetranslationrare"></d-cite> entraîné sur CulturaX et The Stack, utilisant 256 extra_tokens et
les chiffres sont séparés.<br>
Le lecteur pourra trouver le code utilisé <a class="link" href=https://github.com/catie-aq/flashT5/blob/main/examples/fat5-fr/train_tokenizer.py">ici</a>.
</p></div>
<p><br></p>
<h4 id="utiliser-le-bon-optimiseur">Utiliser le bon optimiseur</h4>
<p class="width_125">Changer d’optimiseur par rapport à l’implémentation initiale du modèle peut être judicieux pour accélérer la convergence du modèle
(cela peut néanmoins empêcher la reproduction des résultats du papier original).<br>
Les optimiseurs accélèrent la convergence en permettant des tailles de batchs conséquentes comme dans le cas de LAMB <d-cite bibtex-key="you2020large"></d-cite>
ou l’utilisation de <em>learning rates</em> plus élevés comme Sophia <d-cite bibtex-key="liu2024sophia"></d-cite>.<br>
Des versions plus efficaces des optimiseurs peuvent être aussi utilisées comme l’option <code>fused</code>
dans l’<a class="link" href="https://pytorch.org/docs/stable/generated/torch.optim.Adam.html">optimiseur Adam</a> de PyTorch
ou encore les optimiseurs disponibles dans <a class="link" href="https://github.com/NVIDIA/apex">Apex</a>.</p>
<div class="tip"><p>
Nous avons utilisé l’optimiseur l’optimiseur original du T5, <a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/utils/adamw_scaled.py">AdamWScale</a>.
Pour les valeurs des hyperparamètres, nous utilisons <code>lr = 5e-3</code>, <code>betas = (0.9, 0.999)</code>, <code>eps = 1e-6</code> et <code>weight_decay = 0.0</code>
en nous basant sur les observations de <a class="link" href="https://github.com/PiotrNawrot/nanoT5/issues/25#issuecomment-1922731400">Wilson Wongso</a>.
En effet, il s'avère que tous les optimiseurs alternatifs testés ne convergeaient pas.</p></div>
<div class="note"><p>Nous avons fait en sorte que notre version d'AdamWScale dispose du paramètre <code>foreach</code>.</p>
</div>
<p><br></p>
<h4 id="entra-ner-ses-mod-les-en-bf16-ou-fp16-">Entraîner ses modèles en <code>bf16</code></h4>
<p class="width_125">Les GPU récents permettent d’exploiter pleinement l’utilisation de précision réduite
(permettant de gagner un facteur 2 de débit par rapport au <code>fp32</code>).
Le <code>bf16</code> n’est disponible que sur les architectures Ampere ou plus récentes mais autorise de s’affranchir de méthode
de <em>loss scaling</em> <d-cite bibtex-key="micikevicius2018mixed"></d-cite> qui est généralement nécessaire en <code>fp16</code>
grâce à une plage dynamique plus grande (l’exposant est codé sur 8 bits comme le <code>fp32</code>).</p>
<div class="tip"><p>Dans cette logique, nous entraînons nos modèles en <code>bf16</code>.
Plus précisément, alors qu'au début de nos expérimentations nous utilisions du <code>bf16-mixed</code>, nous avons recouru à
<a class="link" href="https://en.wikipedia.org/wiki/Kahan_summation_algorithm">la sommation compensée de Kahan</a>
afin de pouvoir utiliser du <code>full bf16</code> dans notre optimiseur.<br>
A nouveau, le code de notre optimiseur est consultable <a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/utils/adamw_scaled.py">ici</a>.</p>
</div>
<h4 id="utiliser-moins-de-m-moire-du-gpu">Utiliser moins de mémoire GPU</h4>
<p class="width_125">Certaines techniques existent pour limiter l’utilisation de mémoire GPU par le modèle tel que le
<a class="link" href="https://pytorch.org/docs/stable/checkpoint.html"><em>gradient checkpointing</em></a>
ou les méthodes type ZeRO <d-cite bibtex-key="rajbhandari2020zero"></d-cite> implémentées dans
<a class="link" href="https://github.com/microsoft/DeepSpeed">DeepSpeed</a>.
En limitant la mémoire utilisée, des tailles de batchs plus grandes peuvent être utilisées accélérar l’entraînement du modèle.</p>
<p class="width_125"><br><br></p>
<h3 id="autres">Autres</h3>
<h4 id="le-parall-lisme">Le parallélisme</h4>
<p class="width_125">L’utilisation de plusieurs GPUs est délicate.
Réalisée naïvement, elle peut résulter en des performances inférieures à l’implémentation sur un seul GPU gâchant alors des ressources de calculs.
C’est le cas notamment lorsque des goulets d’étranglement se forment au niveau des communications entre les GPU.
Il s’agit d’être sûr que le modèle n’est pas limité par la bande passante entre les cartes ou de s'assurer que les cartes sont connectées avec des
bandes passantes suffisantes via des techniques type <a class="link" href="https://en.wikipedia.org/wiki/NVLink">NVLink</a> par exemple. </p>
<p class="width_125">A noter aussi que les techniques d’optimisation requièrent en général de synchroniser tous les GPU à la fin d’un batch.
De ce fait, si un GPU est plus lent que les autres (ou est utilisé par un autre processus), le modèle est bridé à la vitesse du GPU le plus lent de l’ensemble. </p>
<div class="note"><p>
Ayant pré-entraîné notre modèle sur une seule A100 80Go, nous n'avons pas pu expérimenter le parallélisme.</p>
</div>
<p><br></p>
<h4 id="les-t-tes-pour-le-finetuning">Les têtes pour le finetuning</h4>
<p class="width_125">Nous nous sommes penchés sur les éléments listés ci-dessus dans une optique d’optimiser le pré-entraînement de notre modèle.
En pratique, nous devons ensuite le finetuner pour le spécialiser sur les tâches finales qui nous intéresse.
Pour cela, nous recourrons à des têtes. Pour le <a class="link" href="https://huggingface.co/docs/transformers/model_doc/t5">T5 « standard »</a>,
cinq sont disponibles dans Transformers permettant d’effectuer toutes les tâches faisables :
<a class="link" href="https://huggingface.co/docs/transformers/model_doc/t5#transformers.T5ForConditionalGeneration"><code>T5ForConditionalGeneration</code></a>,
<a class="link" href="https://huggingface.co/docs/transformers/model_doc/t5#transformers.T5ForSequenceClassification"><code>T5ForSequenceClassification</code></a>,
<a class="link" href="https://huggingface.co/docs/transformers/model_doc/t5#transformers.T5ForTokenClassification"><code>T5ForTokenClassification</code></a>,
<a class="link" href="https://huggingface.co/docs/transformers/model_doc/t5#transformers.T5ForQuestionAnswering"><code>T5ForQuestionAnswering</code></a>
et <a class="link" href="https://huggingface.co/docs/transformers/model_doc/t5#transformers.T5EncoderModel"><code>T5EncoderModel</code></a>.<br><br>
Là encore, un travail d’optimisation peut être effectué.<br>
Pour la génération conditionnelle, le principal point est de s’assurer d’avoir un processus de génération efficace.<br>
Pour les têtes portant sur des tâches de classification (séquence, NER et QA), il faut s’assurer que l’on utilise la partie encodeur
du T5 puisque le décodeur n’est pas essentiel pour celles-ci comme le montre l’EncT5 <d-cite bibtex-key="liu2022enct5"></d-cite>.
En effet, les poids du décodeur prennent inutilement de la place en mémoire, et le temps d’exécution du code du finetuning est doublé inutilement.<br>
La dernière tête sert simplement à ne garder que la partie encodeur d'un modèle encodeur-décodeur. Elle n'a donc pas besoin d'être optimisée.</p>
<div class="tip"><p>
Concernant la tête <code>ForConditionalGeneration</code>, notre
<a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/model/modeling_flash_t5.py">implémentation</a>
repose sur le processus de génération disponible dans le
<a class="link" href="https://github.com/PiotrNawrot/nanoT5/blob/1c82d67bf8dea635be68a3b2a68a43b68b665193/nanoT5/utils/t5_model.py#L407">nanoT5</a>
car est 14% plus rapide que l’implémentation d’Hugging Face.<br>
Concernant les têtes de classification, l’implémentation est disponible dans ce
<a class="link" href="https://github.com/catie-aq/flashT5/blob/main/src/model/custom_heads_flash_t5.py">fichier</a>.
Il s'agit d'un fichier disjoint du fichier modeling car nos implémentations diffèrent de celles disponibles dans Transformers.
En effet, les implémentations des têtes <code>T5ForSequenceClassification</code> et de <code>T5ForQuestionAnswering</code> disponibles dans Transformers reposent
sur l’encodeur et le décodeur du T5 ce qui est donc inefficient.
Nous avons donc recodé ces deux têtes pour n'utiliser que l'encodeur.
Nous avons alors suivi la même structure que la tête <code>T5ForTokenClassification</code> disponible dans Transformers qui,
utilise aussi que l'encodeur et donc reprenons telle quelle. </p>
</div>
<p class="width_125"><br><br><br></p>
<h2 id="benchmark">Benchmark</h2>
<h3 id="TFLOPS">TFLOPS</h3>
<p class="width_125">
Le nombre de TFLOPS (trillions de calculs en virgule flottante qu'un processeur peut effectuer en une seconde) est probablement la mesure la plus parlante
pour étayer l'impact des optimisations effectuées.<br>
Nous comparons alors quatre approches :<br>
• l'implémentation SPDA (Scaled Dot Product Attention) avec full bias,<br>
• la même implémentation mais en Triton,<br>
• l'implémentation en Flash Attention RPE, c'est-à-dire le second noyau que nous avons développé (peut être vu comme le turboT5 mais en C++/Cuda avec bf16 full),<br>
• l'implémentation en Flash Attention i.e. sans biais. Nous l'indiquons pour avoir une référence car elle est inutilisable en pratique pour un T5.<br>
<br>
Pour la passe avant, nous avons :
</p>
<p class="width_125">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/FWD-causal-True_dark.png" width="100%">
<img alt="Benchmark memory backward pass" src="./assets/FWD-causal-True.png" width="100%">
</picture>
<div class="width_125"><p>Pour la passe avant, nous pouvons observer que l'approche en Triton permet 1,34 fois plus de FLOPS que celle en SPDA et que l'approche en Flash Attention RPE permet 1,99 fois plus de FLOPS que celle en SPDA.<br>
Nous pouvons aussi constater que notre implémentation en bf16 est équivalente à du fp16 (faisant même mieux en taille 512).<br>
C'est suite à ce benchmark que nous avons décidé d'entraîner notre modèle en français en bf16, head_dim = 128 et avec une séquence 1024.</p></div>
<br>
<p class="width_125">
Pour la passe arrière, nous avons :
</p>
<p class="width_125">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/BWD-causal-True_dark.png" width="100%">
<img alt="Benchmark memory backward pass" src="./assets/BWD-causal-True.png" width="100%">
</picture>
<div class="width_125"><p>Pour la passe arrière, l'implémentation en Triton se révèle moins efficace que SPDA avec 0,71 fois les FLOPS de SPDA. Celle en Flash Attention RPE est plus ou moins équivalente à SPDA (1,018 fois plus de FLOPS).<br>
Nous pouvons également observer que Triton en head_dim 64 est plus efficace que Triton en head_dim 128.</p></div>
<p><br></p>
<h3 id="torchvstriton">Torch vs Triton</h3>
<p class="width_125">
Nous indiquions plus haut avoir optimisé des parties de l’architecture à l’aide de noyaux Triton ad hoc, à savoir l'entropie croisée et la couche de RMSNorm.
Les benchmarks suivants doivent en illustrer la raison.<br>
Pour l'entropie croisée, nous obtenons une passe avant 7 à 11,4 fois plus rapide, une passe arrière 3,26 à 3,75 plus rapide ainsi qu'une mémoire réduite d'un facteur 4 :</p>
<p class="width_125">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/CE_dark.png" width="100%">
<img alt="Benchmark memory backward pass" src="./assets/CE.png" width="100%">
</picture>
<p class="width_125">
Pour la couche de RMSNorm, nous obtenons une passe avant 3 à 5 fois plus rapide, une passe arrière 2,33 à 4,33 plus rapide ainsi qu'une mémoire réduite d'un facteur 3,2 :</p>
<p class="width_125">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/LN_dark.png" width="100%">
<img alt="Benchmark memory backward pass" src="./assets/LN.png" width="100%">
</picture>
<p class="note">
Notez que l'ensemble des graphiques des benchmarks peuvent être générés automatiquement via le code <a href="https://github.com/catie-aq/flashT5/tree/main/benchmarks">suivant</a>.
</p>
<p><br><br></p>
<h3 id="mod-le-en-fran-ais">Modèle en français</h3>
<p class="width_125">Nous avons appliqué notre travail au français en pré-entraînant un modèle de 147M de paramètres. <br>
Le jeu de données que nous avons utilisé est un mélange de CulturaX, Wikipedia, justice_fr et The Stack. <br>
Notre <em>tokenizer</em> de taille 32 768 (8**5) est entraîné sur CulturaX et The Stack.<br>
Notre modèle est pré-entraîné sur une séquence de 1 024 tokens.</p>
<p class="width_125">
Nous souhaitions comparer les performances de notre modèle face à d'autres modèles en français précédemment publiés comme le CamemBERT <d-cite bibtex-key="Martin_2020"></d-cite> pour les tâches de classification et le BARThez <d-cite bibtex-key="eddine2021barthez"></d-cite> pour les tâches de génération.<br>
Pour cela, il nous est paru important de faire des comparaisons à nombre de <i>tokens</i> vus équivalent.
Nous avons ainsi essayé d'estimer le nombre de <i>tokens</i> vus par ces deux modèles via la formule nombre de steps × la taille de la séquence × la taille du batch. Nous n'avons pas trouvé les informations dans la publication du BARThez pour le faire. Pour le CamemBERT nous l'estimons à environ 419,4 Mds de <i>tokens</i> au maximum. Ce chiffre pourrait être en réalité moins élevé car nous ne connaissons pas le nombre de <i>tokens</i> de <i>padding</i> vus par ce modèle (là où dans notre cas, nous n'en utilisons pas). Ainsi, nous avons pré-entraîné notre modèle sur le nombre maximal de <i>tokens</i> vus par le CamemBERT.<br></p>
<p><br></p>
<p class="width_125">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/loss_train.png" width="49%">
<img alt="Convergence masked accuracy FAT5" src="./assets/loss_train.png" width="49%">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/loss_eval.png" width="49%">
<img alt="Convergence masked accuracy FAT5" src="./assets/loss_eval.png" width="49%">
</picture>
</p>
<p><br></p>
<p class="width_125">
Nous nous sommes également intéressés à comparer notre modèle face à lui-même, c'est-à-dire que nous avons évalué ses performances sur les tâches en aval toutes les 100 000 steps (~26 Mds de tokens) au cours du pré-entraînement.<br>
Dans le tableau ci-dessous, nous avons listés le nombre de <i>tokens</i> équivalents à chaque intervalle de 100 000 steps.<br>
</p>
<table class="width_125">
<thead>
<tr>
<th>Modèle</th>
<th>Nombre de <em>tokens</em> ✝</th>
</tr>
</thead>
<tbody>
<tr>
<td>FAT5-small-100K</td>
<td>26 214 400 000 (100 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-200K</td>
<td>52 428 800 000 (200 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-300K</td>
<td>78 643 200 000 (300 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-400K</td>
<td>104 857 600 000 (400 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-500K</td>
<td>131 072 000 000 (500 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-600K</td>
<td>157 286 400 000 (600 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-700K</td>
<td>183 500 800 000 (700 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-800K</td>
<td>209 715 200 000 (800 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-900K</td>
<td>235 929 600 000 (900 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-1000K</td>
<td>262 144 000 000 (1 000 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-1100K</td>
<td>288 358 400 000 (1 100 000× 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-1200K</td>
<td>314 572 800 000 (1 200 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-1300K</td>
<td>340 787 200 000 (1 300 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-1400K</td>
<td>367 001 600 000 (1 400 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-1500K</td>
<td>393 216 000 000 (1 500 000 × 1024 × 256)</td>
</tr>
<tr>
<td>FAT5-small-1600K</td>
<td>419 430 400 000 (1 600 000 × 1024 × 256)</td>
</tr>
<tr>
<td><a class="link" href="https://hf.co/almanach/camembert-base">camembert (base ou large)</a></td>
<td>419 430 400 000 (100 000 × 512 × 8192)</td>
</tr>
</tbody>
</table>
<p class="width_125">✝ équivaut au nombre de steps × la taille de la séquence × la taille du batch</p>
<p><br></p>
<h4 id="finetuning">Finetuning</h4>
<p class="width_125">Nous nous sommes focalisés sur cinq tâches :<br>
• Du résumé de textes pour illustrer un usage de la tête <code>T5ForConditionalGeneration</code>,<br>
• De la classification binaire pour illustrer un usage de la tête <code>T5ForSequenceClassification</code>,<br>
• De la reconnaissance d’entités nommées pour illustrer un usage de la tête <code>T5ForTokenClassification</code>,<br>
• Du question answering pour illustrer un usage de la tête <code>T5ForQuestionAnswering</code>.<br>
• De la <i>sentence similarity</i> pour illustrer un usage de la tête <code>T5EncoderModel</code>.</p>
<p class="width_125"> Les tâches de classification nous semblent être importantes à évaluer car elles sont généralement ignorées par les benchmarks des grands modèles de langue génératifs alors qu’il de tâches fréquemment utilisées en pratique par les entreprises (recherche documentaire, classification pour d'avis clients, anonymisation de données, etc.).
En témoigne sûrement le fait que 6 ans et demi après sa sortie, BERT <d-cite bibtex-key="devlin2019bert"></d-cite> est téléchargé plus de fois par mois à lui seul que les <a class="link" href="https://huggingface.co/models?pipeline_tag=text-generation&sort=downloads">30 modèles de génération de textes</a> les plus téléchargés sur Hugging Face au moment où nous écrivons ces lignes : 38,5M contre 31,3M.</p>
<p class="width_125">Dans les tableaux suivants, nous soulignons pour le FAT5 la ligne obtenant le meilleur résultat pour chacune des tâches. Nous interprétons les résultats de la partie génération après le tableau sur le résumé de texte. Les résultats sur la partie classification sont interprétés après l'ensemble des tableaux de classification binaire, QA, NER et de <i>sentence-similarity</i>.</p>
<p><br></p>
<h5>Résumé de textes</h5>
<p class="width_125">Pour cette tâche, nous avons utilisé le jeu de données <a class="link" href="https://huggingface.co/datasets/orange_sum">orange_sum</a><d-cite bibtex-key="eddine2021barthez"></d-cite>.</p>
<table class="width_125">
<thead>
<tr>
<th>Modèle</th>
<th>ROUGE-1</th>
<th>ROUGE-2</th>
<th>ROUGE-L</th>
</tr>
</thead>
<tbody>
<tr>
<td>FAT5-small-100K (147M)</td>
<td>28,17</td>
<td>10,60</td>
<td>20,62</td>
</tr>
<tr>
<td>FAT5-small-200K (147M)</td>
<td>28,72</td>
<td>10,86</td>
<td>20,68</td>
</tr>
<tr>
<td>FAT5-small-300K (147M)</td>
<td>28,76</td>
<td>10,85</td>
<td>20,63</td>
</tr>
<tr>
<td>FAT5-small-400K (147M)</td>
<td>28,59</td>
<td>10,76</td>
<td>20,60</td>
</tr>
<tr>
<td>FAT5-small-500K (147M)</td>
<td>28,98</td>
<td>10,97</td>
<td>20,72</td>
</tr>
<tr>
<td>FAT5-small-600K (147M)</td>
<td>29,04</td>
<td>11,20</td>
<td>20,89</td>
</tr>
<tr>
<td>FAT5-small-700K (147M)</td>
<td>28,72</td>
<td>10,87</td>
<td>20,77</td>
</tr>
<tr>
<td>FAT5-small-800K (147M)</td>
<td>29,00</td>
<td>10,91</td>
<td>20,78</td>
</tr>
<tr>
<td>FAT5-small-900K (147M)</td>
<td>29,30</td>
<td>11,34</td>
<td>21,22</td>
</tr>
<tr>
<td>FAT5-small-1000K (147M)</td>
<td>29,10</td>
<td>11,21</td>
<td>21,08</td>
</tr>
<tr>
<td>FAT5-small-1100K (147M)</td>
<td>29,43</td>
<td>11,40</td>
<td>21,15</td>
</tr>
<tr>
<td>FAT5-small-1200K (147M)</td>
<td>29,30</td>
<td>11,38</td>
<td>21,18</td>
</tr>
<tr>
<td>FAT5-small-1300K (147M)</td>
<td>29,38</td>
<td>11,38</td>
<td>21,18</td>
</tr>
<tr>
<td>FAT5-small-1400K (147M)</td>
<td>29,29</td>
<td>11,18</td>
<td>21,14</td>
</tr>
<tr>
<td>FAT5-small-1500K (147M)</td>
<td><u>29,48</u></td>
<td><u>11,48</u></td>
<td><u>21,22</u></td>
</tr>
<tr>
<td>FAT5-small-1600K (147M)</td>
<td>29,30</td>
<td>11,27</td>
<td>21,10</td>
</tr>
<tr>
<td><a class="link" href="https://huggingface.co/moussaKam/barthez">Barthez<d-cite bibtex-key="eddine2021barthez"></d-cite></a> (165M)</td>
<td>31.44</td>
<td>12.77</td>
<td>22.23</td>
</tr>
<tr>
<td><a class="link" href="https://huggingface.co/moussaKam/mbarthez">mBarthez</a> (458M)</td>
<td>32.67</td>
<td>13.73</td>
<td>23.18</td>
</tr>
</tbody>
</table>
<p><br></p>
<p class="width_125">Nous pouvons constater que notre modèle performe moins bien que le Barthez. Nous pouvons émettre quelques hypothèses à ce sujet. <br>
Premièrement, il est vraisemblable que notre processus de génération de texte ne soit pas optimal. Ne connaissant pas celui utilisé par le Barthez, nous avons simplement recouru aux paramètres par défaut de la fonction <a class="link" href="https://github.com/huggingface/transformers/blob/241c04d36867259cdf11dbb4e9d9a60f9cb65ebc/src/transformers/generation/utils.py#L1905">generate</a> de Hugging Face pour ne pas avantager notre modèle avec un processus de génération qui serait plus sophistiqué.<br>
Deuxièmement, nous n'avons pas utilisé de <i>prompt</i> pour conditionner la génération, ce qui aurait pu bénéficier à notre modèle puisque le T5 est le modèle ayant introduit ce système.<br>
Troisièmement, le Barthez a sûrement vu plus de <i>tokens</i> que notre modèle. Bien que nous n'arrivons pas à déterminer ce nombre d'après la publication des auteurs, il est indiqué que c'est un modèle BART <d-cite bibtex-key="lewis2019bartdenoisingsequencetosequencepretraining"></d-cite> qui a reçu un pré-entraînement supplémentaire sur du français. Or dans le papier de BART, il est indiqué que le modèle a été entraîné sur 500000 steps × une séquence de 1 024 <i>tokens</i> × un batch de taille 8000, soit 4 096 000 000 000 <i>tokens</i>, ce qui 9,76 fois plus que notre modèle.
</p>
<p><br></p>
<h5 id="classification">Classification</h5>
<p class="width_125">Nous utilisons une version nettoyée du jeu de données allocine <d-cite bibtex-key="allocine"></d-cite> : <a class="link" href="https://huggingface.co/datasets/CATIE-AQ/allocine_clean">allocine_clean</a>. Plus précisément 0,6 % de l'échantillon de test n’était pas fiable car contenait des fuites ou des données dupliquées. Il est probable que le jeu de données obtenu soit encore imparfait avec notamment des problèmes d'annotations nécessitant un travail de relecture/correction plus important.
</p>
<table class="width_125">
<thead>
<tr>
<th>Modèle</th>
<th>Accuracy</th>
</tr>
</thead>
<tbody>
<tr>
<td>FAT5-small-100K (67,4M)</td>
<td>96,05</td>
</tr>
<tr>
<td>FAT5-small-200K (67,4M)</td>
<td>96,20</td>
</tr>
<tr>
<td>FAT5-small-300K (67,4M)</td>
<td>96,48</td>
</tr>
<tr>
<td>FAT5-small-400K (67,4M)</td>
<td>96,60</td>
</tr>
<tr>
<td>FAT5-small-500K (67,4M)</td>
<td>96,60</td>
</tr>
<tr>
<td>FAT5-small-600K (67,4M)</td>
<td>96,60</td>
</tr>
<tr>
<td>FAT5-small-700K (67,4M)</td>
<td>96,68</td>
</tr>
<tr>
<td>FAT5-small-800K (67,4M)</td>
<td>96,59</td>
</tr>
<tr>
<td>FAT5-small-900K (67,4M)</td>
<td><u>96,75</u></td>
</tr>
<tr>
<td>FAT5-small-1000K (67,4M)</td>
<td>96,62</td>
</tr>
<tr>
<td>FAT5-small-1100K (67,4M)</td>
<td>96,69</td>
</tr>
<tr>
<td>FAT5-small-1200K (67,4M)</td>
<td>96,71</td>
</tr>
<tr>
<td>FAT5-small-1300K (67,4M)</td>
<td>96,69</td>
</tr>
<tr>
<td>FAT5-small-1400K (67,4M)</td>
<td>96,65</td>
</tr>
<tr>
<td>FAT5-small-1500K (67,4M)</td>
<td>96,57</td>
</tr>
<tr>
<td>FAT5-small-1600K (67,4M)</td>
<td>96,69</td>
</tr>
<tr>
<td><a class="link" href="">distilcamembert</a> (68,1M)</td>
<td>96,74</td>
</tr>
<tr>
<td><a class="link" href="https://huggingface.co/bourdoiscatie/camembert_base_cls">camembert-base</a> (111M)</td>
<td>97,27</td>
</tr>
<tr>
<td><a class="link" href="https://huggingface.co/bourdoiscatie/camembert_large_cls">camembert-large</a> (337M)</td>
<td>97,15</td>
</tr>
<tr>
</tbody>
</table>
<p class="width_125">Note : dans le tableau et dans les suivants, distilcamembert se réfère au <a class="link" href="https://huggingface.co/cmarkea/distilcamembert-base">distilcamembert-base</a> <d-cite bibtex-key="delestre2022distilcamembert"></d-cite> que nous avons finetuné.</p>
<p><br></p>
<h5>Reconnaissance d’entités nommées</h5>
<p class="width_125">Pour cette tâche, nous avons utilisé frenchNER dans sa configuration <a class="link" href="https://huggingface.co/datasets/CATIE-AQ/frenchNER_4entities">4 entités</a> (PER, LOC, ORG, MISC) <d-cite bibtex-key="frenchNER2024"></d-cite>.</p>
<table class="width_125">
<thead>
<tr>
<th>Modèle</th>
<th>F1 PER</th>
<th>F1 LOC</th>
<th>F1 ORG</th>
<th>F1 MISC</th>
</tr>
</thead>
<tbody>
<tr>
<td>FAT5-small-100K (67,1M)</td>
<td>96,51</td>
<td>94,48</td>
<td>87,24</td>
<td>75,81</td>
</tr>
<tr>
<td>FAT5-small-200K (67,1M)</td>
<td>96,90</td>
<td>94,83</td>
<td>88,78</td>
<td>76,82</td>
</tr>
<tr>
<td>FAT5-small-300K (67,1M)</td>
<td>97,25</td>
<td>95,11</td>
<td>88,86</td>
<td><u>77,48</u></td>
</tr>
<tr>
<td>FAT5-small-400K (67,1M)</td>
<td>97,18</td>
<td>95,08</td>
<td>89,11</td>
<td>77,42</td>
</tr>
<tr>
<td>FAT5-small-500K (67,1M)</td>
<td>97,25</td>
<td>95,16</td>
<td>89,16</td>
<td>76,91</td>
</tr>
<tr>
<td>FAT5-small-600K (67,1M)</td>
<td>97,19</td>
<td>95,19</td>
<td>88,85</td>
<td>76,88</td>
</tr>
<tr>
<td>FAT5-small-700K (67,1M)</td>
<td>97,17</td>
<td>95,14</td>
<td>89,39</td>
<td>76,82</td>
</tr>
<tr>
<td>FAT5-small-800K (67,1M)</td>
<td><u>97,34</u></td>
<td>95,20</td>
<td>89,18</td>
<td>77,27</td>
</tr>
<tr>
<td>FAT5-small-900K (67,1M)</td>
<td>97,19</td>
<td>95,21</td>
<td>89,04</td>
<td>76,83</td>
</tr>
<tr>
<td>FAT5-small-1000K (67,1M)</td>
<td>97,31</td>
<td>95,26</td>
<td>89,24</td>
<td>76,84</td>
</tr>
<tr>
<td>FAT5-small-1100K (67,1M)</td>
<td>97,11</td>
<td>94,99</td>
<td>88,52</td>
<td>76,30</td>
</tr>
<tr>
<td>FAT5-small-1200K (67,1M)</td>
<td>97,19</td>
<td>95,11</td>
<td>88,79</td>
<td>76,86</td>
</tr>
<tr>
<td>FAT5-small-1300K (67,1M)</td>
<td>97,15</td>
<td>95,00</td>
<td>88,62</td>
<td>76,58</td>
</tr>
<tr>
<td>FAT5-small-1400K (67,1M)</td>
<td>97,22</td>
<td>95,09</td>
<td>89,01</td>
<td>77,00</td>
</tr>
<tr>
<td>FAT5-small-1500K (67,1M)</td>
<td>97,32</td>
<td><u>95,34</u></td>
<td><u>89,39</u></td>
<td>77,30</td>
</tr>
<tr>
<td>FAT5-small-1600K (67,1M)</td>
<td>97,14</td>
<td>95,22</td>
<td>89,24</td>
<td>76,88</td>
</tr>
<tr>
<td><a class="link" href="">distilcamembert</a> (67,5M)</td>
<td>97,26</td>
<td>95,24</td>
<td>89,10</td>
<td>79,88</td>
</tr>
<tr>
<td><a class="link" href="https://huggingface.co/CATIE-AQ/NERmembert-base-4entities">camembert-base</a> (110M)</td>
<td>97,80</td>
<td>95,78</td>
<td>90,27</td>
<td>81,38</td>
</tr>
<tr>
<td><a class="link" href="https://huggingface.co/CATIE-AQ/NERmembert-large-4entities">camembert-large</a> (336M)</td>
<td>98,17</td>
<td>96,37</td>
<td>91,87</td>
<td>83,35</td>
</tr>
</tbody>
</table>
<p><br></p>
<h5 id="question-answering">Question Answering</h5>
<p class="width_125">
Nous avons voulu finetuner notre modèle sur cette tâche mais nous nous sommes rendu compte que notre tokenizer a deux problèmes.<br>
Premièrement, nous avons oublié d'ajouter le token de début de phrase.
Deuxièmement, nous avons décidé d'utiliser un fast BPE tokenizer. Nous avons appris après coup que l'argument `add_special_tokens=True` ne fonctionne pas avec ce type de tokenizer.
Corriger ces deux points nécessite de post-traiter les encodages du tokenizer avant d'effectuer notre tâche de finetuning ce qui n'est pas élégant et nécessite du temps que nous n'avons pas dans l'immédiat.
<p><br></p>
<h5><i>Sentence Similarity</i></h5>
<p class="width_125">
Nous invitons le lecteur à prendre les résultats de cette section avec des pincettes.<br>
Nous avons effectué un finetuning sur cette tâche afin de vérifier que la tête <code>T5EncoderModel</code> fonctionnait
mais nous ne nous focalisons pas sur les résultats obtenus car nous nous interrogeons sur la qualité du benchmark sur lequel nous évaluons les modèles,
à savoir MTEB FR <d-cite bibtex-key="ciancone2024mtebfrenchresourcesfrenchsentence"></d-cite>, une version française de MTEB.<br>
En effet, Nils Reimers, créateur du MTEB, a récemment remis en cause dans un <a class="link" href="https://x.com/Nils_Reimers/status/1870812625505849849">tweet</a>
la pertinence de ce benchmark, le déclarant « mort ».
Plus tôt dans l'année, nous avions d'ailleurs observé des fuites de données et des duplications dans ce benchmark
(voir <a class="link" href="https://huggingface.co/datasets/lbourdois/MTEB_leaks_and_duplications">ici</a> et
<a class="link" href="https://github.com/embeddings-benchmark/mteb/issues/1036">ici</a>).
Alexey Vatolin a ensuite étendu ces observations en prenant également en compte les lignes vides (voir <a class="link" href="https://github.com/embeddings-benchmark/mteb/issues/1049#issuecomment-2463095122">ici</a>).
<br>
Dans le tableau ci-dessous, nous finetunons sur une version nettoyée du jeu de données <code>stsb_multi_mt</code> <d-cite bibtex-key="huggingface:dataset:stsb_multi_mt"></d-cite> (0,653 % du test split n'était pas fiable parce qu'il contenait des fuites ou des données dupliquées) avant d'évaluer sur MTEB FR.
<br>
</p>
<table class="width_125">
<thead>
<tr>
<th>Modèle</th>
<th>Moyenne</th>
<th>Classification</th>
<th>Clustering</th>
<th>PairClassification</th>
<th>Reranking</th>
<th>Retrieval</th>
<th>STS</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>FAT5-small-400K (67,1M)</td>
<td>52,2</td>
<td>59,8</td>
<td>39,1</td>
<td>77,5</td>
<td>56,1</td>
<td>29,1</td>
<td>74</td>
<td>29,8</td>
</tr>
<tr>
<td>distilcamembert(68,1M)</td>
<td>51,3</td>
<td>60,7</td>
<td>37,4</td>
<td>77</td>
<td>51,1</td>
<td>25,2</td>
<td>76,4</td>
<td>31,3</td>
</tr>
</tbody>
</table>
<p><br><br><br></p>
<p class="width_125">
Nous observons dans le graphique de la convergence de la <i>masked accuracy</i>, que les performances de la partie encodeur du modèle progressent dans un premier temps avant de s'aplatir.
</p>
<p><br></p>
<p class="width_125">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/convergence_masked_accuracy_FAT5.png" width="100%">
<img alt="Convergence masked accuracy FAT5" src="./assets/convergence_masked_accuracy_FAT5.png" width="100%">
</picture>
</p>
<p><br></p>
<p class="width_125">
Ce phénomène s'observe aussi dans les résultats des finetunings, le FAT5 match les performances du distilcamembert aux alentours de 800 ou 900K steps (à l'exception de l'entité MISC pour la NER) mais ne fait pas mieux au-delà. Cela est néanmoins encourageant en vue d'un passage à l'échelle puisque les modèles distillés issus de modèles plus importants donnent habituellement de meilleurs résultats que les modèles entraînés de zéro.<br>
Notons que cette forme de plateau dans les performances serait à confirmer en effectuant plusieurs exécutions avec des configurations différentes (au niveau de la <i>seed</i> notamment) pour proposer des résultats sous la forme d'un intervalle au lieu d'un résultat unique (pour chaque step évaluée, nous utilisons une <i>seed</i> de 42).<br>
Signalons également que ce plafonnement pour la partie encodeur a déjà été observé par d'autres auteurs. On peut par exemple citer le CamemBERT(a) 2.0 <d-cite bibtex-key="antoun2024camembert20smarterfrench"></d-cite> qui a également été entraîné sur la partie en français de CulturaX. Le CamemBERT 2.0 n'apparaît pas plus performant que le CamemBERT 1.0 alors qu'il a vu davantage de <i>tokens</i>. Les auteurs obtiennent par contre des gains de performances avec le CamemBERTa 2.0, suggérant ainsi que pour les encodeurs, l'importance est de se focaliser en priorité sur l'architecture (le CamemBERTa 2.0 est un DeBERTaV3 <d-cite bibtex-key="he2023debertav3improvingdebertausing"></d-cite> alors que le CamemBERT 2.0 est un RoBERTa <d-cite bibtex-key="liu2019robertarobustlyoptimizedbert"></d-cite>) plutôt que sur les données. Ce résultat nous invite à réfléchir à une actualisation de l'encodeur du T5.<br>
Une dernière observation pouvant être faite, est que si les performances plafonnent, il est possible de se permettre de stopper le pré-entraînement plus tôt et donc réduire les coûts.<br>
Dans le tableau ci-dessous, nous listons des estimations de coûts (en euros) pour le pré-entraînement de notre modèle selon divers <i>cloud providers</i>.
Pour chacun d'eux, nous nous basons sur le prix horaire d'une A 100 80GB proposé le 20 décembre 2024.<br>
Nous indiquons deux cas, un pré-entraînement sur 262 Mds de <i>tokens</i> (seuil où on observe que les performances sur les tâches de classifications commencent à plafonner et que les gains marginaux deviennent alors faibles) sur 419 Mds de <i>tokens</i> (le nombre de <i>tokens</i> vus au maximum par le CamemBERT).
<br>
</p>
<table class="width_125">
<thead>
<tr>
<th>Cloud provider</th>
<th>Prix horaire d'une A 100</th>
<th>Prix pour 262 Mds de tokens</th>
<th>Prix pour 419 Mds de tokens</th>
<th>Note</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS</td>
<td>1,77</td>
<td>1 616</td>
<td>2 586</td>
<td></td>
</tr>
<tr>
<td>OVH</td>
<td>2,75</td>
<td>2 475</td>
<td>3 960</td>
<td>En optant pour un payement mensuel plutôt qu'horaire, le prix dans les deux cas n'est plus que de 2 200€.</td>
</tr>
<tr>
<td>Azure</td>
<td>3,31</td>
<td>3 021</td>
<td>4 833</td>
<td>Le prix horaire a été calculé à partir du prix mensuel de 8 A100.</td>
</tr>
<tr>
<td>Google Cloud</td>
<td>3,52</td>
<td>3 214</td>
<td>5 143</td>
<td></td>
</tr>
</tbody>
</table>
<br><br>
<p><br><br></p>
<h4>Temps et émissions par pré-entraînement</h4>
<p class="width_125">Les émissions carbones ont été estimées à l’aide du <a class="link" href="https://mlco2.github.io/impact#compute">Machine Learning Impact calculator</a> <d-cite bibtex-key="lacoste2019quantifying"></d-cite>.<br>
Notre modèle a été pré-entraîné sur une A100 PCIe 80GB, sur une infrastructure privée.
Pour l'efficacité carbone, nous nous sommes basés sur les chiffres journaliers indiqués
par <a class="link" href="https://app.electricitymaps.com/zone/FR">electricitymaps</a> pour la France lors de la période de notre pré-entraînement.<br>
Les finetunings ont été effectués pour leur part sur une A100 PCIe 40GB.
Le temps d’exécution se comptant généralement en heures voire en minutes, pour l’efficacité carbone nous nous référons alors au chiffre d’electricitymaps indiqué pour l’heure en question plutôt que pour le chiffre journalier.<br>
Nous estimons ainsi les émissions de notre modèle à 14,084 kg eq. CO2,
dont 13,5 kg eq. CO2 pour le pré-entraînement et 0,584 kg eq. CO2 pour les 49 finetunings.<br>
À ce chiffre, nous devons ajouter des émissions supplémentaires estimées à 6,24 kg eq. CO2.
Elles correspondent au finetuning de modèles pour établir les baselines auxquelles se comparer (0,475 kg eq. CO2), à nos travaux préliminaires en bf16 mixed (4,735 kg eq. CO2 pour le pré-entraînement de trois modèles différents sur 300K steps) et à des tests en bf16 full avant l'entraînement de notre modèle final (1,03 kg eq. en pré-entraînement d'un modèle deux fois plus petit sur 400K steps).<br>
Ainsi, au total, nous estimons l’empreinte carbone de nos travaux à 20,324 kg eq. CO2. </p>
<p class="width_125">Sur la phase de pré-entraînement (nous n’avons pas assez d’informations pour faire des estimations pour les autres phases),
il est alors possible de nous situer vis-à-vis des autres modèles en français pré-entraînés listés précédemment : </p>
<table class="width_125">
<thead>
<tr>
<th>Modèle</th>
<th>Temps (H)</th>
<th>Emissions (kg Co2 eq)</th>
<th>Note</th>
</tr>
</thead>
<tbody>
<tr>
<td>Camembert</td>
<td>6 144</td>
<td>106,91 ✝</td>
<td>24H × 256 Tesla V100-SXM2-32GB à 58g (moyenne sur 2019) <br>Les auteurs ne précisent pas les chiffres pour la version large</td>
</tr>
<tr>
<td>Flaubert base <d-cite bibtex-key="le2020flaubert"></d-cite></td>
<td>13 120</td>
<td>190,24 à 228,29 ✝</td>
<td>410H × 32 V100 à 58g (moyenne sur 2019) <br>Le type de la V100 n’est pas spécifié<br>(V100-SXM2-32GB ? V100-SXM2-16GB ? V100-PCIE-16GB ?)</td>
</tr>
<tr>
<td>Flaubert large <d-cite bibtex-key="le2020flaubert"></d-cite></td>
<td>49 920</td>
<td>723,84 à 868,61 ✝</td>
<td>390H × 128 V100 à 58g (moyenne sur 2019) <br>Le type de la V100 n’est pas spécifié<br>(V100-SXM2-32GB ? V100-SXM2-16GB ? V100-PCIE-16GB ?)</td>
</tr>
<tr>
<td>Barthez</td>
<td>7 680 ★</td>
<td>107,52 à 129,02 ✝</td>
<td>60H × 128 V100 à 56g (moyenne sur 2020) <br>Le type de la V100 n’est pas spécifié<br>(V100-SXM2-32GB ? V100-SXM2-16GB ? V100-PCIE-16GB ?)</td>
</tr>
<tr>
<td>FAT5-small</td>
<td>1 461</td>
<td>13,5</td>
<td>1 461H × 1 A100 à 36,96 g (moyenne entre le 18/10/2024 et le 19/12/2024)</td>
</tr>
</tbody>
</table>
<p class="width_125">✝ les chiffres indiqués sont des estimations à partir des informations fournies par les auteurs dans leur publication<br>
★ nous indiquons uniquement les heures pour le pré-entraînement en français appliqué par dessus le pré-entraînement en anglais initial sur lequel se base le modèle</p>
<p><br></p>
<h3 id="mod-les-en-anglais">Modèles dans d'autres langues</h3>
<p class="width_125">
Notre contribution se focalise sur le français avec l'introduction d'un nouveau modèle. Pour d'autres langues, nous ne pouvons pas nous permettre d’effectuer un travail de la même envergure.<br>
Néanmoins, nous mettons à disposition un <a class="link" href="https://github.com/catie-aq/flashT5/blob/main/convert_huggingface_t5.py">code</a> permettant d'adapter vers notre méthode des poids de (m)T5/FLAN-T5 <d-cite bibtex-key="chung2022scaling"></d-cite> déjà pré-entraînés. Nous espérons ainsi que les utilisateurs pourront poursuivre efficacement le pré-entraînement d'un de ces modèles pour l'adapter à des données plus récentes par exemple.<br>
Notez cependant que cette adaptation est limitée puisque le pré-entraînement supplémentaire devra être effectué dans la précision du modèle original. Par exemple, si les poids du modèle sont en fp32 (ce qui est le cas du FLAN-T5), l'entraînement ne sera pas aussi rapide que le FAT5 qui est en bf16.<br><br>
Pour les anglophones, nous avons déjà adapté les poids des différentes versions du FLAN-T5 à notre méthode. Tous les poids peuvent être trouvés dans cette
<a class="link" href="https://huggingface.co/collections/CATIE-AQ/catie-english-fat5-flan-662b679a8e855c7c0137d69e">collection Hugging Face</a>.<br><br>
Si vous souhaitez pré-entraîner votre propre modèle (pour être spécialisé dans un domaine spécifique par exemple, et ainsi bénéficier d'un <i>tokenizer</i> personnalisé), nous vous renvoyons à nouveau vers le <a class="link" href="https://github.com/catie-aq/flashT5/tree/main/examples/minipile">tutoriel</a> indiquant comment procéder pour pré-entraîner un modèle sur minipile. Notez que nous avons testé et entraîné le modèle du tutoriel sur une A100, cela peut ou non fonctionner avec d'autres GPU.</p>
<p class="width_125"><br><br><br></p>
<h2 id="la-suite">La suite</h2>
<p class="width_125">Terminons cet article en évoquant ce que nous comptons, ou du moins aimerions, donner comme suite à ce travail.<br></p>
<h3>À court terme</h3>
<p class="width_125">Il s'agit ici de choses qui auraient déjà dû être présentes dans cet article mais qui ont pris plus de temps que prévu.
Typiquement, nous avons terminé la conception des jeux de données mais n'avons pas eu le temps d'effectuer les finetunings.<br>
L'objectif est d'effectuer ces tâches prochainement pour pouvoir ajouter les résultats obtenus dans une actualisation de cet article de blog.
</p>
<h4>Corriger le tokenizer</h4>
<p class="width_125">
Le FAT5 actuel est utilisable. Néanmoins, du fait des problèmes avec le <i>tokenizer</i> entraînant des post-traîtement inélégant pour certaines tâches, nous n'excluons pas de ré-entraîner un modèle (sur 1M de steps seulement) avec un nouveau <i>tokenizer</i> permettant un usage plus simple du modèle.
<br><br></p>
<h4>Modèle instruct</h4>
<p class="width_125">Nous souhaiterions tester les capacités de génération de textes du FAT5 de façon plus optimale via notamment l'usage de <i>prompts</i> en développant un modèle instruct.<br>
Pour cela, nous disposons du <a class="link" href="https://huggingface.co/datasets/CATIE-AQ/DFP">DFP</a> (<i>Dataset of French Prompts</i>) <d-cite bibtex-key="centre_aquitain_des_technologies_de_l'information_et_electroniques_2023"></d-cite>, un jeu de données de plus de 100M de lignes portant sur trente tâches de NLP. Il reprend la méthodologie du jeu de données <a class="link" href="https://huggingface.co/datasets/bigscience/xP3">xP3</a> ayant servi au mT0 <d-cite bibtex-key="muennighoff2023crosslingualgeneralizationmultitaskfinetuning"></d-cite>. Nous pourrions à cette occasion également vérifier le "Finding 2" de BigScience <d-cite bibtex-key="wang2022languagemodelarchitecturepretraining"></d-cite> (page 9 de la publication) indiquant que les modèles encodeur-décodeur auraient de meilleures capacités en 0-shot que les modèles décodeur. <br>
Au-delà du NLP, nous possédons aussi plus de 2M de lignes de prompt de type "open QA" qui doivent nous permettre de tester le FAT5 sur des tâches/connaissances plus généralistes.<br><br>
La conception de ce modèle instruct doit en outre nous permettre de travailler sur son alignement, notamment via un jeu de données de 12M de lignes pour effectuer de la DPO en français.<br><br><br></p>
<h4>Longues séquences</h4>
<p class="width_125">
Le pré-entraînement effectué porte sur des séquences de 1 024 <i>tokens</i>. Or, le noyau CUDA que nous avons développé prend en compte des encodages positionnels permettant d'étendre fortement cette taille de contexte ainsi qu'une inférence linéaire.<br>
Dans cette logique, nous avons créé deux jeux de données de longues séquences en français (un de QA, un de résumé de textes) sur lesquels nous souhaitons finetuner notre modèle.<br><br></p>
<h3>À long terme</h3>
<p class="width_125">Les éléments listés ci-dessous portent sur des pistes à plus long terme. C'est-à-dire que leur implémentation prendra du temps et feront l'objet d'un nouvel article de blog le cas échéant.</p>
<h4 id="calcul-lin-aire">Réduction de la mémoire</h4>
<p class="width_125">Bien que déjà satisfaits par les optimisations effectuées sur la mémoire via notre noyau CUDA, nous pensons que nous pouvons pousser ces résultats plus loin via d'autres techniques. Par exemple, nous pouvons citer la méthode CCE (Cut Cross-Entropy) <d-cite bibtex-key="wijmans2024cut"></d-cite> avec laquelle nous avons déjà obtenus des résultats intéressants sur des modèles décodeur.<br>
De plus, alors que nous nous sommes concentrés sur le pré-entraînement, un travail serait à faire sur l'inférence qui en pratique consomme le plus de ressources dans le temps une fois le modèle en production. Nous pensons notamment à utiliser la SageAttention2 <d-cite bibtex-key="zhang2024sageattention2efficientattentionthorough"></d-cite> sortie pendant que notre modèle s'entraînait.
<br><br></p>
<h4 id="calcul-lin-aire">Calcul linéaire</h4>
<p class="width_125">Dans ce travail, nous présentons un modèle à mémoire linéaire.
Une amélioration supplémentaire consisterait à ce qu’en plus de cette mémoire, le modèle opère avec des calculs linéaires.<br>
L’idée est de substituer l’attention quadratique traditionnelle par une autre forme d’attention.<br>
On peut penser à certaines déjà appliquées au T5, comme celle du LongT5 <d-cite bibtex-key="guo2022longt5"></d-cite>.
Il est aussi envisageable de tester des formes plus récentes comme Based <d-cite bibtex-key="arora2024simple"></d-cite>.
Nous sommes également intéressés par effectuer des tests avec Hedgehog <d-cite bibtex-key="zhang2024hedgehog"></d-cite>.
En effet, il est possible de les associer aux noyaux optimisés disponibles dans <a class="link" href="https://github.com/HazyResearch/ThunderKittens/tree/main/kernels">ThunderKittens</a> <d-cite bibtex-key="thunderkittens"></d-cite>.
L’intérêt est qu’il est alors possible de garder le modèle pré-entraîné et, via un finetuning supplémentaire, de remplacer l’attention standard avec softmax par une linéaire avec Hedgehog.<br>
LoLCATs <d-cite bibtex-key="zhang2024lolcatslowranklinearizinglarge"></d-cite> effectue ce finetuning via LoRA <d-cite bibtex-key="hu2021loralowrankadaptationlarge"></d-cite>.
<br><br></p>
<h4 id="passage-l-chelle">Taille des modèles</h4>
<p class="width_125"> Des T5/FLAN-T5 ont été entraînés jusqu'à 11 milliards de paramètres, montrant ainsi que cette architecture peut passer à l'échelle.<br>
Nous aimerions ainsi proposer des modèles de taille plus importante avec un FAT5-base et un FAT5-large de respectivement 305M et 973M de paramètres que nous souhaiterions ensuite distiller. L'objectif est de proposer des modèles consommant le moins possible en routine/inférence.<br>
Nous nous attendons également à ce que les modèles distillés donnent de meilleures performances que des modèles de taille équivalente entraînés de zéro.<br>
Cela doit nous permettre également de proposer des modèles qui seront utilisés en pratique. En effet, en l'état actuel pour le français, si l'utilisateur est davantage motivé par les performances plutôt que par la taille mémoire du modèle, il a davantage intérêt à utiliser un CamemBERTa 2.0 pour les tâches de classification. Le présent FAT5 doit ainsi davantage être vue comme une preuve de concept avant un passage à l'échelle qui doit le rendre compétitif.
<br><br></p>
<h4 id="modeles-specialises">Les données d'entraînement</h4>
<p class="width_125">
Dans le cadre de ce travail, nous avons utilisé des données en français « générique » principalement issues de CulturaX. Pendant l'entraînement de notre modèle,
Hugging Face a introduit le jeu de données FineWeb2 <d-cite bibtex-key="penedo2024fineweb-2"></d-cite> qui comporte du français. Nous aimerions pré-entraîné un nouveau modèle pour pouvoir comparer l'impact des données de pré-entraînement sur les performances des tâches en aval.<br>
Au-delà du français générique, nous souhaitons surtout pouvoir appliquer notre méthodologie à des domaines spécifiques (médecine, variante régionale du français, etc.).<br>
Pour cela, il faudrait entraîner un nouveau <em>tokenizer</em> dédié et effectuer un nouveau pré-entraînement pour chacun des domaines choisis.
L’intérêt des optimisations mises en place et présentées dans cet article de blog est de permettre une réduction importante du coût du pré-entraînement.<br>
Nous souhaiterions ensuite mener une comparaison entre ces petits modèles spécialisés vs. de grands modèles génériques.<br><br></p>
<h4 id="modeles-specialises">Une actualisation de l'architecture du T5</h4>
<p class="width_125">La dernière piste que nous souhaiterions explorer est une actualisation de l'architecture du T5. En effet, les encodeurs-décodeurs ayant été délaissés, ils n'ont pas bénéficié des améliorations qu'ont reçues ces derniers mois les modèles décodeurs (couches d'activation ou de normalisation plus récentes, <i>multi token prediction</i> <d-cite bibtex-key="gloeckle2024betterfasterlarge"></d-cite>, etc.).</p>
<p class="width_125"><br><br><br></p>
<h2 id="conclusion">Conclusion</h2>
<p class="width_125">
Nous avons introduit le modèle FAT5 (Flash Attention T5) en détaillant notre démarche d’optimisation de différents éléments des processus de pré-entraînement et de finetuning.
Celui-ci se base sur des noyaux permettant d'utiliser la Flash Attention avec un T5 et de donner une mémoire linéaire au modèle.
Nous avons notamment appliqué nos travaux au français en guise de preuve de concept et fait en sorte qu’il soit aussi utilisable dans n'importe quelle autre langue.
Nous espérons que notre méthode, permettant de pré-entraîner de zéro un modèle de 147M de paramètres pour 1 600€, pourra être utile aux personnes disposant de ressources de calculs limitées.
Elle ouvre également une voie vers un retour à un usage de modèles encodeur-décodeur plutôt qu’uniquement décodeur.<br>
<p class="width_125"><br><br></p>
<style>
d-appendix .citation {
font-size: 11px;
line-height: 15px;
border-left: 1px solid rgba(0, 0, 0, 0.1);
padding-left: 10px;
border: 1px solid rgba(0,0,0,0.1);
background: #0D1117;
padding: 10px 10px;
border-radius: 3px;
color: rgba(150, 150, 150, 1);
overflow: hidden;
margin-top: -12px;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
<h3 id="citation">Citation</h3>
<pre class="citation long">@misc {FAT5,
title = { FAT5: Flash Attention T5 },
author = { Boris ALBAR and Loïck BOURDOIS },
organization = { Centre Aquitain des Technologies de l'Information et Electroniques },
year = 2025,
url = { https://huggingface.co/spaces/CATIE-AQ/FAT5-report },
doi = { 10.57967/hf/4160 },
publisher = { Hugging Face }
}</pre>
<d-appendix style="color: #9CA3AF;" >
<d-bibliography src="bibliography.bib"></d-bibliography>
</d-appendix>
</d-article>
<script>
const article = document.querySelector('d-article');
const toc = document.querySelector('d-contents');
if (toc) {
const headings = article.querySelectorAll('h2, h3, h4');
let ToC = `<nav role="navigation" class="l-text figcaption" style="color: #9CA3AF;"><h3>Table des matières</h3>`;
let prevLevel = 0;
for (const el of headings) {
// should element be included in TOC?
const isInTitle = el.parentElement.tagName == 'D-TITLE';
const isException = el.getAttribute('no-toc');
if (isInTitle || isException) continue;
el.setAttribute('id', el.textContent.toLowerCase().replaceAll(" ", "_"))
const link = '<a target="_self" href="' + '#' + el.getAttribute('id') + '">' + el.textContent + '</a>';
const level = el.tagName === 'H2' ? 0 : (el.tagName === 'H3' ? 1 : 2);
while (prevLevel < level) {
ToC += '<ul>'
prevLevel++;
}
while (prevLevel > level) {
ToC += '</ul>'
prevLevel--;
}
if (level === 0)
ToC += '<div>' + link + '</div>';
else
ToC += '<li>' + link + '</li>';
}
while (prevLevel > 0) {
ToC += '</ul>'
prevLevel--;
}
ToC += '</nav>';
toc.innerHTML = ToC;
toc.setAttribute('prerendered', 'true');
const toc_links = document.querySelectorAll('d-contents > nav a');
window.addEventListener('scroll', (_event) => {
if (typeof (headings) != 'undefined' && headings != null && typeof (toc_links) != 'undefined' && toc_links != null) {
// Then iterate forwards, on the first match highlight it and break
find_active: {
for (let i = headings.length - 1; i >= 0; i--) {
if (headings[i].getBoundingClientRect().top - 50 <= 0) {
if (!toc_links[i].classList.contains("active")) {
toc_links.forEach((link, _index) => {
link.classList.remove("active");
});
toc_links[i].classList.add('active');
}
break find_active;
}
}
toc_links.forEach((link, _index) => {
link.classList.remove("active");
});
}
}
});
}
</script>
</body>
</html> |