GPU Của Tôi Chậm Hơn Cả CPU Mà Nó Thay Thế

Có lần tôi chuyển một phép tính nặng từ CPU sang GPU, mong rằng nó sẽ bay vùn vụt. Thay vào đó, nó bò lê — chậm hơn một cách đáng kinh ngạc so với đoạn mã thông thường mà lẽ ra nó phải thay thế. Tôi đã bỏ tiền mua tốc độ và bằng cách nào đó lại lắp đặt sự chậm chạp.

Phần cứng không phải là nút thắt cổ chai. Một GPU hiện đại là một trong những thứ nhanh nhất bạn có thể gắn vào một chiếc máy tính. Vấn đề là tôi đã giao cho nó công việc được định hình theo một cách mà nó dở tệ gần như độc nhất vô nhị, rồi lại ngạc nhiên khi nó không chạy nổi.

Bản nâng cấp hóa ra là bản hạ cấp

Kỳ vọng thì đơn giản và, tôi tưởng vậy, an toàn: GPU thì nhanh, công việc của tôi thì chậm, nên chuyển công việc lên GPU thì nó sẽ nhanh. Đó là phiên bản dân gian về cách tăng tốc vận hành, và nó sai theo một cách cụ thể, đầy tính răn dạy.

Khi tôi đo phiên bản “đã tăng tốc”, nó không chậm hơn một chút. Nó chậm hơn gấp nhiều lần — cái loại kết quả cho bạn biết vấn đề không phải là thiếu một tối ưu hóa nào đó mà là một sự lệch pha căn bản. Có điều gì đó về hình dạng của những gì tôi đang làm đang chủ động chống lại phần cứng.

”Đưa nó lên GPU” thực sự có nghĩa là gì

Đây là điều tôi đã thực sự làm. Tôi có một vòng lặp sẵn có: lấy một đơn vị công việc, tính toán nó, chuyển sang cái tiếp theo, hàng nghìn lần như vậy. Để “dùng GPU”, tôi chỉ đơn giản cho mỗi đơn vị riêng lẻ chạy trên GPU — và để nguyên vòng lặp ở chính chỗ cũ của nó.

Thế là hàng nghìn lần liên tiếp, chương trình của tôi quay sang GPU, giao cho nó một tác vụ nhỏ, chờ đợi câu trả lời, rồi quay lại cho cái tiếp theo. Vòng lặp vẫn nắm quyền điều khiển. GPU chỉ là một nhà thầu cực nhanh mà tôi gọi điện thoại, từng cái một, cho mỗi một cái đinh tôi muốn đóng.

Cái giá ẩn: chỉ riêng việc nhờ vả đã tốn kém

Mỗi lần bạn giao một mẩu công việc cho GPU, có một chi phí cố định để thiết lập và điều phối nó — nhỏ khi đứng một mình, vô hình nếu bạn chỉ trả một lần. Tôi đã trả nó hàng nghìn lần này qua hàng nghìn lần khác. Khi nhìn kỹ, phần lớn thời gian chạy hoàn toàn không phải là tính toán. Đó là phần phụ trội: GPU ngồi không, chờ được bảo phải làm gì, trong khi chương trình của tôi dành thời gian hỏi hết câu hỏi tí hon này đến câu hỏi tí hon khác.

Tôi đã xây dựng một quy trình mà gần như toàn bộ là hành động hỏi han, với công việc thực sự chỉ là một sai số làm tròn. Phiên bản CPU đã chậm, nhưng ít nhất nó luôn bận rộn. Phiên bản GPU chậm vì nó dành phần lớn cuộc đời mình để chờ đợi.

Một GPU là chiếc xe buýt, không phải chiếc taxi

Mô hình tư duy cuối cùng đã sửa được nó: GPU không phải là một cách nhanh hơn để làm một việc. Nó là một cách để làm một số lượng khổng lồ cùng một việc cùng một lúc. Nó là chiếc xe buýt năm mươi chỗ, không phải chiếc xe thể thao.

Gọi nó một lần cho mỗi mục cũng giống như cho chiếc xe buýt đó chạy đi chạy lại khắp thành phố với mỗi chuyến chỉ một hành khách. Chiếc xe buýt thực sự nhanh. Thông lượng của bạn thì thảm hại — và nó thảm hại chính vì bạn đang dùng một phương tiện được chế tạo cho đám đông để chở từng người một. Lỗi không nằm ở chiếc xe buýt. Nó nằm ở cách điều phối.

Cách sửa: ngừng vòng lặp, bắt đầu gộp lô

Việc sửa chữa không phải là một kernel nhanh hơn hay một card tốt hơn. Đó là tái cấu trúc vòng lặp. Thay vì “với mỗi mục, gửi nó lên GPU”, tôi định hình lại công việc để GPU có thể nhận nhiều mục cùng lúc và xử lý chúng chung với nhau — biến hàng nghìn lượt điều phối tí hon thành một nhúm lượt điều phối lớn. Phần phụ trội mà tôi đã trả đi trả lại sụp đổ thành thứ mà tôi chỉ trả vài lần thay vì hàng nghìn lần.

Đây là một dạng phổ biến của cái mà người ta gọi là “vector hóa” hay gộp lô: diễn đạt nhiều phép toán tương tự nhau cùng nhau thay vì như một chuỗi dài các bước nhỏ. Nó thường là sự khác biệt giữa một GPU làm bẽ mặt CPU của bạn và một GPU bị CPU làm bẽ mặt, và khoảng cách giữa hai bên có thể là khổng lồ.

Vì sao đây mới chính là toàn bộ ý nghĩa của phần cứng

Tôi mất một thời gian để thấm nhuần lý do vì sao cấu trúc lại quan trọng đến thế, thay vì coi nó như một chi tiết tinh chỉnh. Lợi thế của GPU ở đây không đến từ việc bất kỳ một phép toán đơn lẻ nào nhanh. Nó đến từ việc làm một số lượng khổng lồ các phép toán tương tự nhau cùng một lúc. Tính song song đó chính là toàn bộ giá trị cốt lõi. Nếu mã của bạn không thể diễn đạt công việc của nó dưới dạng “làm tất cả những thứ này cùng một lúc”, thì chẳng có gì để tính song song bám vào — và một GPU chạy công việc tuần tự, từng cái một, chỉ là một CPU đắt tiền, vụng về, méo mó về hình dạng.

Thế nên “đưa nó lên GPU” chưa bao giờ là chỉ thị thực sự. Chỉ thị thực sự là “định hình lại công việc để nó có thể được làm tất cả cùng một lúc”. Phần cứng chỉ có thể nhanh đến mức mà hình dạng của bài toán cho phép.

Công cụ không thể cứu vãn một hình dạng sai

Bài học này khái quát hóa vượt xa khỏi GPU. Việc vớ lấy phần cứng nhanh hơn chẳng giúp được mấy nếu cấu trúc của phép tính đang chống lại phần cứng đó. Với loại công việc như thế này, hiệu năng nằm ở hình dạng của phép tính nhiều hơn — cách nó được tổ chức, gộp lô, và diễn đạt — chứ không phải ở tốc độ danh nghĩa của thứ đang chạy nó. Bạn có thể đặt một thuật toán chậm lên một cỗ máy nhanh và nhận được một nỗi thất vọng nhanh chóng.

Tôi luôn ghi nhớ sự chậm chạp đó như một lời nhắc rằng “tôi đã làm nó nhanh hơn” và “tôi đã chuyển nó sang phần cứng nhanh hơn” không phải là cùng một câu nói. Câu thứ nhất là về việc hiểu được công việc. Câu thứ hai là về việc hy vọng rằng đám silicon sẽ hiểu nó giùm bạn. Nó sẽ không hiểu đâu.

— Không tín hiệu, không lợi nhuận, không phải lời khuyên đầu tư.