Niższe opóźnienie i wyższa przepustowość dzięki DeepSeek
Wdrożenie wielo-GPU zwiększa wydajność modelu MoE.
W większości systemów opóźnienie i przepustowość są często sprzecznymi celami, które wymagają kompromisów podczas projektowania i wdrażania. Na przykład w gęstych, wielkoskalowych modelach językowych zwiększenie rozmiaru partii (batch size) może poprawić przepustowość, ale jednocześnie zwiększa opóźnienie; zwiększenie równoległości tensorowej w obrębie pojedynczej maszyny może zmniejszyć opóźnienie, ale zmniejsza liczbę replik, prowadząc do niższej przepustowości.
Modele typu Mixture of Experts (MoE), takie jak DeepSeek-V3/R1, wykazały ostatnio doskonałe możliwości modelowe oraz wydajność operacyjną. Przykładowo, model DeepSeek-V3/R1 posiada łącznie 671 miliardów parametrów, jednak każdy token wykorzystuje podczas wnioskowania tylko 37 miliardów parametrów. Architektura tego modelu stanowi wyzwanie, ale też otwiera nowe możliwości dla systemów wnioskowania.
Niniejszy artykuł wykazuje, że w przeciwieństwie do konwencjonalnych systemów, modele MoE takie jak DeepSeek-V3/R1 mogą w większości scenariuszy jednocześnie osiągać wyższą przepustowość i niższe opóźnienie przy wykorzystaniu większej liczby jednostek GPU we wdrożeniach wielowęzłowych.
Architektury wdrożeniowe
Ze względu na dużą liczbę małych ekspertów w modelu, wdrożenia muszą być rozproszone na wiele urządzeń. Rozważyliśmy zarówno wdrożenia jednowęzłowe na pojedynczym węźle z 8xH200 GPU, jak i wdrożenia wielowęzłowe na 8xH100 GPU.
Obie architektury wdrożeniowe wykorzystują równoległość danych (Data Parallelism), koordynowaną przez nasz wewnętrzny harmonogram żądań. Implementacja równoległości danych obejmuje uruchomienie wielu instancji silnika wnioskowania, z których każda działa niezależnie, obsługując i utrzymując żądania. Harmonogram żądań, który komunikuje się z silnikiem poprzez GRPC, jest odpowiedzialny za jak najbardziej równomierne rozprowadzanie żądań, jednocześnie ułatwiając ponowne użycie pamięci podręcznej KV (KV Cache) poprzez wysyłanie żądań z częściowo dopasowanym prefiksem do serwerów zawierających tę pamięć. Instancje silnika nie obejmują wielu węzłów. Mogą one opcjonalnie wykorzystywać równoległość tensorową do dzielenia mechanizmu uwagi (attention) między wiele urządzeń. Instancje są połączone za pomocą NVLink w przypadku jednowęzłowym lub InfiniBand w przypadku wielowęzłowym, wysyłając i gromadząc ekspertów.
Jednowęzłowa konfiguracja wdrożeniowa zapewnia lepsze opóźnienie przy małych rozmiarach partii, jednak wydajność gwałtownie spada w warunkach zwiększonego obciążenia.
W celu wdrożenia silnika obsługującego żądania uruchamiamy jeden pod na węzeł, który hostuje wiele instancji silnika. PyTorch odpowiada za ustanowienie komunikacji rozproszonej i negocjację inicjalizacji NVSHMEM. Do komunikacji wykorzystujemy własne kernele CUDA opisane we wcześniejszym wpisie na blogu. Implementacja obu wdrożeń jest praktycznie identyczna, a model dobiera odpowiednie kernele w oparciu o architekturę implementującą równoległość ekspertów.
Techniki zrównoległania
Przed przejściem do porównań wydajności należy zrozumieć kluczowe strategie zrównoleglania, które umożliwiają wdrażanie ogromnych modeli MoE, takich jak DeepSeek-V3/R1.
Równoległość tensorowa
We wnioskowaniu LLM, równoległość tensorowa (TP) jest zazwyczaj stosowana w celu zmniejszenia zużycia pamięci i obliczeń na jednostkę GPU, co tym samym redukuje opóźnienie. Zazwyczaj możemy dzielić warstwy Linear Projection w warstwach Attention i MLP wzdłuż wymiarów wierszy lub kolumn, oraz dzielić operacje Attention wzdłuż wymiaru głowicy uwagi.
Dzięki TP architektura Llama-3 nie powiela obliczeń dla operacji Linear Projection i Attention między jednostkami GPU, co stanowi idealną metodę podziału (sharding). Jednak w modelach DeepSeek-V3/R1, TP nie może tego osiągnąć.
Modele DeepSeek-V3/R1 wykorzystują Multi-Latent Attention (MLA). Warstwa MLA najpierw wykorzystuje Linear Projection kv_a_proj do obliczenia wektora ukrytego, a następnie używa kolejnego Linear Projection kv_b_proj do przekształcenia go w przestrzeń każdej głowicy uwagi. Ponieważ wszystkie głowice uwagi współdzielą ten sam wektor ukryty, TP nie może dzielić tego wektora, więc wszystkie rangi TP muszą replikować parametry i obliczenia kv_a_proj oraz kv_b_proj. Podobnie, ponieważ MLA przechowuje wektor ukryty w pamięci KV Cache, każda ranga TP przechowuje identyczną kopię KV Cache.
Pomimo pewnego powielania w MLA, równoległość tensorowa nadal zapewnia częściową redukcję zapotrzebowania na obliczenia, co czyni ją wartościową w scenariuszach wymagających wysokich prędkości wyjściowych.
Równoległość ekspertów
Modele DeepSeek-V3/R1 zastępują warstwy MLP warstwami MoE. Warstwa MoE posiada 256 kierowanych ekspertów i jednego wspólnego eksperta. Każdy token jest wysyłany do 8 różnych ekspertów w celu obliczeń, a wyniki są sumowane ważone. Każdy token obliczany jest również u wspólnego eksperta, a wynik jest dodawany do wyniku uzyskanego od kierowanych ekspertów.
Równoległość ekspertów (EP) służy jako typowe podejście do podziału warstw MoE, przy czym każda jednostka GPU zarządza 256 / EP kierowanymi ekspertami, zachowując kopię wspólnego eksperta. W porównaniu z TP, zaletą EP jest możliwość rozproszenia obliczeń na większą liczbę jednostek GPU, co zmniejsza obciążenie obliczeniowe i zużycie pamięci na jednostkę GPU.
Przed wykonaniem obliczeń przez ekspertów, wszystkie jednostki GPU muszą wykonać komunikację AllToAll, aby rozesłać tokeny do jednostek GPU, w których znajdują się odpowiedni eksperci; po zakończeniu obliczeń przez ekspertów wymagana jest kolejna komunikacja AllToAll, aby zebrać wyniki obliczeń z różnych jednostek GPU i wykonać sumowanie ważone. Zaimplementowaliśmy zoptymalizowaną wersję tych dwóch kerneli komunikacyjnych AllToAll, Dispatch i Combine, wykorzystując NVSHMEM. W naszym poprzednim wpisie na blogu szczegółowo opisaliśmy implementację, a nasze kernele zostały udostępnione jako open-source na GitHubie.
Równoległość danych
Dzięki EP możemy rozproszyć obliczenia MoE na 128 lub nawet więcej jednostek GPU. Jednak obliczenia MLA nie mogą być partycjonowane za pomocą EP. W tym momencie możemy wprowadzić równoległość danych (DP). Każda grupa DP posiada pełną kopię warstwy MLA. Każda grupa DP przyjmuje różne dane wejściowe i wykonuje obliczenia warstwy MLA niezależnie.
DP i TP warstwy MLA można łączyć, przy czym jedna grupa DP jest dzielona na wiele rang TP. EP warstwy MoE można łączyć z DP/TP warstwy MLA. EP = DP * TP. Przykładowo, na 16 maszynach, konfiguracja EP128 DP32 TP4 oznacza rozproszenie kierowanych ekspertów na 128 jednostek GPU, gdzie każde 4 jednostki GPU tworzą grupę DP, łącznie dając 32 niezależne grupy DP.
Wdrożenie jednowęzłowe a wielowęzłowe
671 miliardów parametrów modelu DeepSeek przekracza pojemność pamięci pojedynczej maszyny z 8xH100 GPU (80 GB * 8), jednak pojedyncza maszyna z 8xH200 GPU może w pełni pomieścić cały model (141 GB * 8). Przy zastosowaniu konfiguracji EP8 DP8 TP1 model wykorzystuje około 100 GB pamięci na GPU, pozostawiając około 40 GB na KV Cache i inne wyniki pośrednie. Jeden token zajmuje 70 272 bajty KV Cache. Zakładając, że każde żądanie ma 5000 tokenów, każda jednostka GPU może obsłużyć około 100 żądań.
Chcieliśmy zrozumieć różnice w wydajności między wdrożeniami jednowęzłowymi a wielowęzłowymi w różnych konfiguracjach. Użyliśmy jednej maszyny H200 do wdrożenia jednowęzłowego i do 16 maszyn H100 do wdrożeń wielowęzłowych. Dla każdego środowiska wdrożeniowego użyliśmy kombinacji TP 1, 2, 4, 8 oraz rozmiarów partii na jednostkę GPU równych 1, 2, 4, 8, 16, 32, 64, 128. Założyliśmy, że każde żądanie ma długość KV Cache wynoszącą 5000 tokenów. Założyliśmy również, że Multi-Token Prediction (MTP) przewiduje 1 dodatkowy token (tj. długość zapytania każdego żądania wynosi 2) i konserwatywnie przyjęliśmy wskaźnik akceptacji na poziomie 60%. Poniższy rysunek przedstawia przepustowość i prędkość wyjściową dla różnych konfiguracji.
Oś pozioma reprezentuje prędkość wyjściową na żądanie w tokenach na sekundę. Oś pionowa wykorzystuje skalę logarytmiczną do pokazania przepustowości na maszynę w tokenach na sekundę. Oznaczyliśmy Pareto Frontier dla każdej konfiguracji EP liniami w różnych kolorach.
W scenariuszach z wymaganiami ekstremalnie wysokiej prędkości wyjściowej, użycie jednowęzłowej konfiguracji EP8 DP1 TP8 z rozmiarem partii 1 może osiągnąć prędkość wyjściową przekraczającą 100 tokenów/s, ale przepustowość jest ekstremalnie niska, równoważna prędkości wyjściowej. W tym scenariuszu cała partia zawiera tylko 2 tokeny, które mogą być wysłane do co najwyżej 2*8=16 ekspertów, aktywując łącznie co najwyżej 57 miliardów parametrów.
W zakresie prędkości wyjściowej 80-40 tokenów/s, wraz ze wzrostem przepustowości, prędkość wyjściowa znacząco maleje. W przeciwieństwie do tego, EP128 ma około 5-krotnie wyższą przepustowość niż wdrożenie jednowęzłowe przy tej samej prędkości wyjściowej.
To zjawisko można wyjaśnić badając zachowanie wdrożeń jednowęzłowych: zwiększenie rozmiaru partii bezpośrednio koreluje ze wzrostem liczby aktywowanych ekspertów. Gdy rozmiar partii wynosi 1, średnia liczba aktywowanych ekspertów na GPU wynosi 2 * 8 / 8 = 2. Gdy partia jest wystarczająco duża, wszyscy eksperci są aktywowani, co oznacza, że każda jednostka GPU aktywuje 256 / 8 = 32 ekspertów. Aktywowanie większej liczby ekspertów oznacza, że jednostka GPU musi odczytać więcej parametrów z pamięci, co znacząco zwiększa presję na przepustowość pamięci. Ponieważ faza dekodowania dużych modeli językowych jest już ograniczona przepustowością pamięci, a nie wydajnością obliczeniową, zwiększanie rozmiaru partii w wdrożeniu jednowęzłowym znacząco redukuje prędkość wyjściową.
Porównanie czterech konfiguracji wdrożeń wielowęzłowych (EP16, EP32, EP64 i EP128) ujawnia, że wyższe wartości EP przesuwają Pareto Frontier w kierunku jednoczesnej poprawy przepustowości i prędkości wyjściowej.
Użycie wyższej liczby EP oznacza, że każdej jednostce GPU przypisuje się mniej ekspertów. Na przykład EP128 oznacza, że każda jednostka GPU jest odpowiedzialna za 256 / 128 = 2 ekspertów, dzięki czemu presja na przepustowość pamięci jest znacząco zredukowana. Innymi słowy, używając większej liczby EP, efektywnie zyskujemy większą przepustowość pamięci. Gdy rozmiar partii na GPU jest mniejszy niż 64, zwiększanie rozmiaru partii nie wpływa znacząco na szybkość obliczeń ekspertów, ponieważ zwiększenie liczby danych wejściowych nie zwiększa znacząco presji na przepustowość pamięci. Dlatego obserwujemy, że przy użyciu EP128 zwiększanie rozmiaru partii nie wpływa na prędkość wyjściową w tak znaczącym stopniu.
Co ciekawe, przy większych rozmiarach partii (64 żądania na GPU) zaobserwowaliśmy nowe zjawisko: przepustowość wdrożenia jednowęzłowego jest nieco wyższa niż wdrożenia wielowęzłowego. Częściowo przyczyną jest fakt, że wewnątrz węzła NVLink ma wyższą przepustowość niż międzywęzłowy InfiniBand. Inna część wynika z ograniczeń w naszej implementacji. Przeanalizujemy to zjawisko bardziej szczegółowo w późniejszym terminie.
Z uwagi na ograniczenia pojemności pamięci, konfiguracja EP8 DP8 TP1 nie może osiągnąć rozmiaru partii 128 na GPU, więc wdrożenie wielowęzłowe jest nadal lepszym wyborem w scenariuszach dążących do wyższej przepustowości.
Nakładanie się obliczeń i komunikacji
Jak wspomniano powyżej w odniesieniu do równoległości ekspertów, jednostki GPU pozostają bezczynne podczas komunikacji warstwy MoE. Aby zmniejszyć marnotrawstwo i obniżyć opóźnienie, musimy znaleźć zadania obliczeniowe niezależne od danych, aby wypełnić ten czas bezczynności.
Górna część powyższego rysunku przedstawia przepływ obliczeń jednej warstwy. Obliczenia MoE zależą od Dispatch, a obliczenia następnej warstwy zależą od wyniku Combine.
Umieszczamy wspólnego eksperta na każdej jednostce GPU. Dzięki temu obliczenia wspólnego eksperta nie wymagają komunikacji AllToAll. Dlatego możemy przeprowadzić obliczenia wspólnego eksperta natychmiast po Dispatch Send, a następnie czekać na zakończenie Dispatch Recv. Nazywamy ten schemat nakładania się "Dispatch Overlap".
Dispatch Overlap oferuje prostą implementację i szerokie zastosowanie. Ta technika ukrywa czas obliczeń wspólnego eksperta we wszystkich rozmiarach EP i rozmiarach partii.
Aby jeszcze bardziej zwiększyć nakładanie się obliczeń i komunikacji, użyliśmy mikro-partii (micro-batching) wspomnianych w raporcie technicznym DeepSeek, aby zerwać zależność danych. Jak pokazano w dolnej części rysunku, podzieliliśmy obliczenia jednej warstwy Transformer na 5 etapów:
Etap 1: InputNorm, QKVProj, AppendKV, BMM
Etap 2: BMM, Attn, OProj, PostNorm, Gate
Etap 3: Dispatch Send, Shared Expert
Etap 4: Dispatch Recv, MoE, Combine Send
Etap 5: Combine Recv
W pierwszych 3 warstwach Dense Transformer używamy całej partii. W kolejnych 58 warstwach MoE Transformer równomiernie dzielimy partię na dwie mikro-partie. Dwie mikro-partie wykonują się naprzemiennie, z przesunięciem o 3 etapy. Ponieważ między tymi dwiema mikro-partiami nie ma zależności danych, możemy przełączyć się na obliczenia innej mikro-partii po Dispatch Send i po Combine Send.
Analiza opóźnień
Następnie porównujemy efekty nakładania się poprzez eksperyment, a także porównujemy różnice w wydajności między wdrożeniem jednowęzłowym EP8 a wielowęzłowym EP128. Dla ułatwienia porównania użyliśmy jednostek GPU H100 do poniższego eksperymentu. Użyliśmy TP1, rozmiaru partii 128 na jednostkę GPU, długości zapytania 2 na żądanie i długości KV Cache 5000.
Powyższy rysunek przedstawia całkowity czas spędzony na jednej warstwie MoE Transformer oraz proporcję opóźnień różnych typów kerneli. Z wyjątkiem Dispatch, Combine i GroupGEMM, czas wykonania innych kerneli powinien być równy w seriach EP8, EP128 NoOverlap i EP128 DispatchOverlap, ponieważ rozmiar partii jest taki sam.
Nakładanie się (Overlapping)
Porównajmy najpierw efekty trzech metod nakładania. NoOverlap zajęło łącznie 2667µs, DispatchOverlap zajęło 2651µs, oszczędzając 16µs, czyli zaledwie 0,6%. MicroBatch pokazał bardzo znaczącą poprawę, zajmując 1896µs, co stanowi przyspieszenie o 29%. Czas operacji Dispatch i Combine został znacząco zredukowany. Dispatch spadł z 593µs do 367µs, a Combine z 1012µs do 237µs.
Należy zauważyć, że w przypadku kerneli obliczeniowych podzielenie partii o rozmiarze 128 na dwie partie o rozmiarze 64 zwiększa całkowity czas wykonania. Dlatego, mimo że czas spędzony na komunikacji zmniejszył się o 1001µs, całkowity czas zmniejszył się tylko o 771µs. Wyjaśnimy powód, używając modelu Roofline w poniższej sekcji.
Z tego powodu mikro-partie nie zawsze poprawiają wydajność.
Powyższy rysunek pokazuje poprawę wydajności Microbatch w porównaniu z DispatchOverlap dla rozmiarów partii 4-128. Gdy rozmiar partii jest mniejszy niż 32, Microbatch zmniejsza wydajność o 5%-40%. Gdy rozmiar partii jest większy lub równy 32, Microbatch może poprawić wydajność o 10%-35%.
EP8 kontra EP128
Wróćmy do poprzedniego rysunku i porównajmy EP8 oraz EP128 Microbatch. EP8 zajęło łącznie 1802µs, nieco mniej niż 1896µs w przypadku EP128. Oprócz wspomnianego powyżej zwiększonego czasu wykonania kerneli spowodowanego przez Microbatch, główne różnice występują w GroupGEMM używanym do obliczeń MoE oraz w dwóch kernelach komunikacyjnych: Dispatch i Combine.
GroupGEMM dla EP8 zajęło 555µs, podczas gdy GroupGEMM dla EP128 zajęło 270µs, co stanowi redukcję o połowę. To kluczowa zaleta wdrożenia wielowęzłowego.
Niestety, czas spędzony na komunikacji wzrósł o 213µs, co znacząco zniwelowało przewagę GroupGEMM. W oddzielnych testach wydajności naszych kerneli komunikacyjnych stwierdziliśmy, że mogą one osiągnąć tylko połowę przepustowości InfiniBand. Będziemy kontynuować optymalizację naszych kerneli komunikacyjnych.
Innym kernelem, który znacząco pozostaje w tyle, jest GEMM. Microbatch zwiększył GEMM o 95µs. Przeanalizujemy GEMM głębiej w sekcji Roofline poniżej. Wierzymy, że obecna implementacja GEMM nie osiągnęła jeszcze optymalnej wydajności.
Roofline
Model Roofline to dobre narzędzie do analizy wydajności kerneli. Jego osią poziomą jest intensywność arytmetyczna, czyli stosunek FLOP do bajtów I/O pamięci. Wartość na osi poziomej można obliczyć bezpośrednio z semantyki kernela. Oś pionowa reprezentuje osiągniętą wydajność, obliczoną przez podzielenie FLOP przez opóźnienie wzorcowe.
Teoretyczna górna granica wydajności kernela jest bezpośrednio określona przez specyfikację jednostki GPU. Szczytowa wydajność FP8 jednostki H100 wynosi 1979 TFLOP/s, co jest przedstawione jako linia pozioma w modelu Roofline. Przepustowość pamięci jednostki H100 wynosi 3,35 TB/s, co jest przedstawione jako nachylenie linii przechodzącej przez początek układu współrzędnych. Te dwie linie wyznaczają limity wydajności odpowiednio dla kerneli ograniczonych obliczeniowo i ograniczonych pamięciowo.
Poniżej omawiamy wydajność kerneli GroupGEMM i GEMM.
GroupGEMM
Kernel GroupGEMM w MoE wykonuje następujące obliczenia: Istnieje łącznie g grup, i-ta grupa ma m_i tokenów, wykonując mnożenie macierzy [m_i, k] x [k, n] -> [m_i, n]. W testach wydajności zakładamy, że liczba tokenów w każdej grupie jest taka sama, oznaczona jako m_i = m. Wtedy liczba FLOP dla GroupGEMM wynosi 2 * g * m * k * n, a liczba bajtów I/O pamięci to g * (m * k + n * k + m * n).
W modelu DeepSeek-V3/R1 istnieje 256 ekspertów, a każdy token jest wysyłany do 8 ekspertów w celu obliczeń. Zakładając rozmiar partii 128, długość zapytania 2, przy użyciu konfiguracji EP128 DP128, średnia liczba tokenów otrzymywanych przez każdego eksperta (tj. m) wynosi 128 * 2 * 8 * 128 / 256 = 1024. Podobnie możemy obliczyć m dla innych konfiguracji i rozmiarów partii.
Do testów wydajności użyliśmy implementacji GroupGEMM z DeepGEMM. Punkty testowe obejmowały kombinacje konfiguracji EP8, EP16, EP32, EP64, EP128 z TP1 i rozmiarami partii 1-128.
Powyższy rysunek pokazuje model Roofline dla GroupGEMM w różnych konfiguracjach EP. Różne EP odpowiadają różnym liczbom grup. Rysunek ilustruje niemal nakładające się linie wydajności, co wskazuje, że wydajność GroupGEMM jest w przeważającej mierze determinowana przez całkowitą liczbę tokenów (reprezentowaną jako g * m).
Gwiazdki oznaczają punkty danych odpowiadające rozmiarowi partii 128 na GPU dla każdej konfiguracji EP. Porównując te oznaczone gwiazdką punkty danych, widzimy, że wraz ze wzrostem EP (i synchronicznym wzrostem DP), liczba tokenów na eksperta m również rośnie. Przy EP8, m=128, podczas gdy przy EP128, m=2048.
Wraz ze wzrostem m, rośnie również intensywność arytmetyczna. W większości konfiguracji GroupGEMM jest ograniczony przepustowością pamięci, więc zwiększenie m poprawia wydajność.
GEMM
Kernel GEMM odpowiada za Linear Projections w modelu, takie jak Q/K/V/O Projection. Dla mnożenia macierzy [m, k] x [k, n] -> [m, n], liczba FLOP wynosi 2 * m * k * n, a bajty I/O pamięci to m * k + n * k + m * n. Możemy również przetestować opóźnienie dla rozmiarów partii 1-128.
Powyższy rysunek pokazuje model Roofline dla GEMM w różnych konfiguracjach EP. Widzimy, że wydajność GEMM jest ograniczona przepustowością pamięci. Wraz ze wzrostem rozmiaru partii, rośnie również intensywność arytmetyczna, co poprawia wydajność.
Microbatch
Używając mikro-partii, dzielimy partię równomiernie na dwie części. Z powyższych dwóch rysunków widzimy, że gdy m staje się m/2, wydajność mnożenia macierzy spada. Dlatego wykonanie dwóch mnożeń macierzy o rozmiarze m/2 zajmuje więcej czasu niż wykonanie jednego mnożenia macierzy o rozmiarze m.
Multi-Token Prediction
W całym artykule zakładaliśmy użycie Multi-Token Prediction (MTP) do dekodowania spekulatywnego. MTP zmienia długość zapytania na żądanie z 1 na 2. Dla mnożenia macierzy jest to równoważne zmianie m na m * 2, co zwiększa wydajność mnożenia macierzy. Z drugiej strony, gdybyśmy narysowali model Roofline dla MLA, stwierdzilibyśmy, że zwiększenie długości zapytania znacząco zwiększa wydajność kernela MLA.
Dlatego użycie MTP odgrywa ważną rolę w wydajności modelu.
Wdrożenie i optymalizacja
W tej sekcji przedstawimy niektóre szczegóły implementacji i optymalizacji dla naszego modelu DeepSeek-V3/R1.
Kwantyzacja
Model DeepSeek-V3/R1 został natywnie wytrenowany w formacie FP8 z użyciem schematu kwantyzacji blokowej, gdzie wagi są kwantyzowane statycznie, a aktywacje w czasie rzeczywistym. Zamiast statycznego obliczania współczynnika skalowania na kanał lub macierz, współczynniki skalowania są obliczane dla wektorów 128-elementowych dla aktywacji oraz płytek 128x128 elementów dla macierzy, co ogranicza spadek dokładności wynikający z kwantyzacji.
W Perplexity polegamy na połączeniu kerneli CUDA i Triton do obsługi wnioskowania, przy czym CUDA jest używana do najbardziej wrażliwych na wydajność i rzadko modyfikowanych kerneli (takich jak attention i GEMM), podczas gdy Triton implementuje szeroki zakres kerneli aktywacji, normalizacji i narzędziowych. Triton pozwolił nam szybko dostosować kernele do schematu kwantyzacji blokowej.
Dla warstw liniowych i MoE mieszamy kernele Deep GEMM z naszymi własnymi kernelami Triton GEMM, ponieważ zauważyliśmy, że dla określonych wymiarów macierzy i małych rozmiarów partii Split-K zapewnia niższe opóźnienie. Jeśli warstwa niekwantyzowana wykonuje mnożenie (M, K) x (K, N), wymaga to współczynnika skalowania (M x ceil_div(K, 128)) x (ceil_div(K, 128), ceil_div(N, 128)) dla kwantyzacji blokowej. W przypadku kwantyzacji blokowej współczynniki skalowania dla aktywacji są obliczane na bieżąco, zamiast być wstępnie kalibrowane. Ponieważ współczynniki skalowania aktywacji są agregowane tylko wzdłuż wymiaru K, a nie M, kernele wymagają tylko niewielkich zmian, aby wspierać ten schemat.
Funkcja aktywacji SiLU używana przez DeepSeek-V3/R1 wymagała znacznych zmian, aby wspierać grafy CUDA, kwantyzację blokową i dynamicznie kierowane liczby tokenów. Kwantyzacja blokowa może być problematyczna, ponieważ wprowadza redukcje poziome, jednak kernel już dzielił aktywacje wzdłuż ich wymiaru ukrytego na bloki po 1024 elementy. Wewnątrz jednego bloku tensor do zakwantyzowania był dalej dzielony na bloki po 128 w celu obliczenia największej wartości bezwzględnej, przy czym Triton generował wydajne redukcje typu cross-warp max, dodając minimalny narzut.
Aby wspierać kierowanie (routing) MoE w ramach grafów CUDA, kernele muszą być świadome informacji o kierowaniu, wskazujących liczbę tokenów na eksperta, zamiast planować pracę na podstawie rozmiaru buforów przydzielonych do utrzymania górnej granicy liczby tokenów. Nie możemy dzielić problemu na podstawie wymiarów tensora wejściowego, więc uruchamiamy stałą liczbę trwałych (persistent) kerneli, które odczytują informacje o kierowaniu, aby ustalić, ile tokenów jest zapełnionych, i dynamicznie dzielą pracę przetwarzania aktywacji między nimi.
Przenieśliśmy już niektóre z naszych kerneli do projektu FlashInfer, a w przyszłości będziemy udostępniać więcej naszego kodu jako open-source.
Warstwa MLA
Do obliczeń MLA używamy FlashInfer. FlashInfer obsługuje elastyczne ustawienia tabeli stron (Page Table) i zapewnia ekstremalnie wysoką wydajność.
Połączyliśmy q_a_proj i kv_a_proj w jeden qkv_a_proj. Opóźnienie spadło z 15,4 µs + 14,8 µs = 30,2 µs do 16,7 µs.
Rozłożyliśmy kv_b_proj na dwie macierze: k_b_proj i v_b_proj. Napisaliśmy kernel FP8 Block Quantized BMM do obliczeń związanych z tymi dwiema macierzami.
Graf CUDA
Graf CUDA może znacząco zmniejszyć narzut uruchamiania kernela, co jest kluczowe dla wydajności. Tworzymy graf CUDA dla każdego rozmiaru partii.
Przed opracowaniem naszego kernela AllToAll używaliśmy torch.all_to_all_single() do komunikacji AllToAll. Operacja ta wymaga, aby wszystkie jednostki GPU używały tego samego rozmiaru partii. Jednak różne grupy DP mogą uruchamiać różne rozmiary partii.
Aby zapewnić kompatybilność all_to_all_single() z różnymi grupami DP używającymi różnych rozmiarów partii, najpierw użyliśmy operacji allreduce() przed każdym uruchomieniem modelu, aby uzyskać maksymalny rozmiar partii wśród wszystkich grup DP. Następnie sprawiliśmy, że wszystkie grupy DP używały tego rozmiaru partii do uruchomienia.
Chociaż to podejście zapewnia możliwość użycia grafu CUDA, ma trzy wady. Po pierwsze, wymaga dodatkowej operacji allreduce(). Po drugie, grupy DP z mniejszymi rozmiarami partii są zmuszone do dopełniania (padding). Po trzecie, sprawia, że nasz kod implementacyjny jest złożony.
Po zaimplementowaniu własnego kernela AllToAll nie wymagamy już, aby wszystkie jednostki GPU używały tego samego rozmiaru partii. Dlatego nie musimy już wykonywać dodatkowych operacji allreduce() ani dopełniać rozmiarów partii.
Router MoE
Router MoE jest zaimplementowany w Tritonie, opierając się na zmodyfikowanym sortowaniu pochodzącym z biblioteki standardowej, które śledzi również indeksy posortowanych elementów. Implementacja jest współdzielona przez wszystkie modele MoE, ponieważ kierowanie w Mixtral jest szczególnym przypadkiem tras DeepSeek, gdzie grupa Top-K jest taka sama jak grupa wszystkich ekspertów. Kernele rzadkie (sparse) pobierają indeksy i wyniki Top-K bezpośrednio, podczas gdy schematy gęstego (dense) Dispatch/Combine polegające na all-to-all wymagają agregacji informacji o kierowaniu na eksperta, zamiast na token.
Przyszłe prace
W przyszłych pracach planujemy dalej optymalizować wydajność modelu DeepSeek.
Najważniejszą kolejną optymalizacją jest dezagregacja Prefill. Faza Prefill i faza Decode modelu DeepSeek-V3/R1 mają bardzo różne charakterystyki obliczeniowe. Obie mogą wykorzystywać różne strategie optymalizacji i schematy wdrożeniowe.
Dla warstwy MLA, w fazie Decode używamy absorpcji macierzy (Matrix Absorption) w celu zmniejszenia liczby FLOP obliczeń MLA. W fazie Prefill, rzutowanie wektora ukrytego na przestrzeń K/V, a następnie obliczanie w formie Multi-Head Attention (MHA), działałoby lepiej.
Jeśli Prefill i Decode działają na tej samej jednostce GPU, w celu zmniejszenia wpływu Prefill na prędkość wyjściową Decode, zazwyczaj używamy dzielonego prefill (chunked prefill) w celu podzielenia zapytania na wiele fragmentów. Ponieważ KV Cache przechowuje wektor ukryty, przekształcenie MLA w formę MHA staje się trudne.
Dla warstwy MoE, w fazie Decode używamy tak dużego EP i DP, jak to możliwe, aby zwiększyć liczbę tokenów wejściowych na eksperta, tym samym poprawiając wydajność GroupGEMM. W fazie Prefill, ponieważ liczba tokenów jest już wystarczająco duża, GroupGEMM jest już ograniczony obliczeniowo. Dlatego dla Prefill możemy użyć mniejszego EP i DP.
Jeśli Prefill i Decode działają na tej samej jednostce GPU, dopóki jakakolwiek grupa DP wykonuje Prefill, opóźnienie warstw MoE na wszystkich jednostkach GPU wzrośnie, znacząco wpływając na prędkość wyjściową Decode.
Oprócz dezagregacji Prefill, planujemy również zoptymalizować następujące aspekty:
Wydajność AllToAll: Nasz kernel AllToAll obecnie osiąga tylko 1/3 przepustowości InfiniBand. Będziemy kontynuować optymalizację tego kernela.
Dekodowanie spekulatywne w stylu EAGLE: W powyższych danych założyliśmy użycie dekodowania spekulatywnego do przewidzenia 1 tokena. EAGLE może używać struktury drzewiastej do przewidywania wielu tokenów, poprawiając długość akceptacji, co może znacząco zwiększyć prędkość wyjściową.
Kernel GEMM: W modelu Roofline pokazanym wcześniej możemy stwierdzić, że wydajność kernela GEMM jest wciąż daleka od teoretycznego limitu. Będziemy kontynuować optymalizację tego kernela.
GB200 NVL72: W najnowszym rozwiązaniu NVIDIA GB200 NVL72, 72 jednostki Blackwell GPU są połączone za pomocą szybkiego NVLink. Dla modeli o architekturze MoE jest to ogromna szansa i wyzwanie.
Wnioski
Wielowęzłowe wdrożenie modeli DeepSeek MoE osiąga to, co zazwyczaj jest niemożliwe w przypadku gęstych modeli LLM: jednoczesną poprawę przepustowości i opóźnienia. Dzięki rozproszeniu ekspertów na większą liczbę jednostek GPU zmniejszamy presję na przepustowość pamięci na urządzenie, umożliwiając szybsze przetwarzanie i wyższą przepustowość systemu. Nasze eksperymenty pokazują, że konfiguracje EP128 osiągają do 5-krotnie wyższą przepustowość przy równoważnych prędkościach wyjściowych w porównaniu z wdrożeniami jednowęzłowymi.
Techniki nakładania się obliczeń i komunikacji, takie jak mikro-partie, znacząco redukują narzut komunikacji wielowęzłowej, a nasze wdrożenie wykazuje przyspieszenie do 40%. Nasze niestandardowe kernele komunikacyjne AllToAll i zoptymalizowane implementacje kerneli umożliwiły wydajne wdrożenie modelu z 671 miliardami parametrów.
W miarę jak architektury MoE zyskują popularność dzięki swoim możliwościom, te strategie wdrożeniowe dostarczają cennych spostrzeżeń dotyczących wydajnego skalowania takich modeli.
Bibliografia
Przegląd systemu wnioskowania DeepSeek-V3/R1
Megatron-LM: Trenowanie wielomiliardowych modeli językowych z użyciem równoległości modeli
EAGLE: Próbkowanie spekulatywne wymaga przemyślenia niepewności cech
Cytat
@online{perplexity2025LowerLatencyAndHigherThroughputWithMultiNodeDeepseekDeployment,
author = {{Perplexity} and Lequn Chen and Nandor Licker and Erin McKnight},
title = {{Lower latency and higher throughput with multi-node DeepSeek deployment}},
date = {2025-04-18},
url = {https://perplexity.coolkeypoint.com/hub/blog/lower-latency-and-higher-throughput-with-multi-node-deepseek-deployment}
}