12.4. Phần cứng

Để xây dựng các hệ thống có hiệu năng cao, ta cần nắm chắc kiến thức về các thuật toán và mô hình để có thể biểu diễn được những khía cạnh thống kê của bài toán. Đồng thời, ta cũng cần có một chút kiến thức cơ bản về phần cứng thực thi ở bên dưới. Nội dung trong phần này không thể thay thế một khóa học đầy đủ về phần cứng và thiết kế hệ thống, mà sẽ chỉ đóng vai trò như điểm bắt đầu để giúp người đọc hiểu tại sao một số thuật toán lại hiệu quả hơn các thuật toán khác và làm thế nào để đạt được thông lượng cao. Thiết kế tốt có thể dễ dàng tạo ra sự khác biệt rất lớn, giữa việc có thể huấn luyện một mô hình (ví dụ trong khoảng một tuần) và không thể huấn luyện (ví dụ mất 3 tháng để huấn luyện xong, từ đó không kịp tiến độ). Ta sẽ bắt đầu bằng việc quan sát tổng thể một hệ thống máy tính. Tiếp theo, ta sẽ đi sâu hơn và xem xét chi tiết về CPU và GPU. Cuối cùng, ta sẽ tìm hiểu cách các máy tính được kết nối với nhau trong trạm máy chủ hay trên đám mây. Cần lưu ý, phần này sẽ không hướng dẫn cách lựa chọn card GPU. Nếu bạn cần gợi ý, hãy xem Section 19.5. Phần giới thiệu về điện toán đám mây trên AWS có thể tìm thấy tại Section 19.3.

Bạn đọc có thể tham khảo nhanh thông tin tóm tắt trong Fig. 12.4.1. Nội dung này được trích dẫn từ bài viết của Colin Scott trình bày tổng quan về những tiến bộ trong thập kỉ qua. Số liệu gốc được trích dẫn từ buổi thảo luận của Jeff Dean tại trường Stanford năm 2010. Phần thảo luận dưới đây sẽ giải thích cơ sở cho những con số trên và cách mà chúng dẫn dắt ta trong quá trình thiết kế thuật toán. Nội dung khái quát và ngắn gọn nên nó không thể thay thế một khóa học đầy đủ, nhưng sẽ cung cấp đủ thông tin cho những người làm mô hình thống kê để có thể đưa ra lựa chọn thiết kế phù hợp. Để có cái nhìn tổng quan chuyên sâu về kiến trúc máy tính, bạn đọc có thể tham khảo [Hennessy & Patterson, 2011] hay một khóa học gần đây của Arste Asanovic.

../_images/latencynumbers.png

Fig. 12.4.1 Số liệu về độ trễ mà mọi lập trình viên nên biết.

12.4.1. Máy tính

Hầu hết những nhà nghiên cứu học sâu đều được trang bị hệ thống máy tính có bộ nhớ và khả năng tính toán khá lớn với một hay nhiều GPU. Những máy tính này thường có những thành phần chính sau:

  • Bộ xử lý, thường được gọi là CPU, có khả năng thực thi các chương trình được nhập bởi người dùng (bên cạnh chức năng chạy hệ điều hành và các tác vụ khác), thường có 8 lõi (core) hoặc nhiều hơn.
  • Bộ nhớ (RAM) được sử dụng để lưu trữ và truy xuất các kết quả tính toán như vector trọng số, giá trị kích hoạt và dữ liệu huấn luyện.
  • Một hay nhiều kết nối Ethernet với tốc độ đường truyền từ 1 Gbit/s tới 100 Gbit/s (các máy chủ tân tiến còn có các phương thức kết nối cao cấp hơn nữa).
  • Cổng giao tiếp bus mở rộng tốc độ cao (PCIe) kết nối hệ thống với một hay nhiều GPU. Các hệ thống máy chủ thường có tới 8 GPU được kết nối với nhau theo cấu trúc liên kết phức tạp. Còn các hệ thống máy tính thông thường thì có 1-2 GPU, phụ thuộc vào túi tiền của người dùng và công suất nguồn điện.
  • Bộ lưu trữ tốt, thường là ổ cứng từ (HDD) hay ổ cứng thể rắn (SSD), được kết nối bằng bus PCIe giúp truyền dữ liệu huấn luyện tới hệ thống và sao lưu các checkpoint trung gian một cách hiệu quả.
../_images/mobo-symbol.svg

Fig. 12.4.2 Kết nối các thành phần máy tính

Hình Fig. 12.4.2 cho thấy, hầu hết các thành phần (mạng, GPU, ổ lưu trữ) được kết nối tới GPU thông qua đường bus PCI mở rộng. Đường truyền này gồm nhiều làn kết nối trực tiếp tới CPU. Ví dụ, Threadripper 3 của AMD có 64 làn PCIe 4.0, mỗi làn có khả năng truyền dẫn 16 Gbit/s dữ liệu theo cả hai chiều. Bộ nhớ được kết nối trực tiếp tới CPU với tổng băng thông lên đến 100 GB/s.

Khi ta chạy chương trình trên máy tính, ta cần trộn dữ liệu ở các bộ xử lý (CPU hay GPU), thực hiện tính toán và sau đó truyền kết quả tới RAM hay ổ lưu trữ. Do đó, để có hiệu năng tốt, ta cần đảm bảo rằng chương trình chạy mượt mà và hệ thống không có nút nghẽn cổ chai. Ví dụ, nếu ta không thể tải ảnh đủ nhanh, bộ xử lý sẽ không có có dữ liệu để chạy. Tương tự, nếu ta không thể truyền các ma trận tới CPU (hay GPU) đủ nhanh, bộ xử lý sẽ thiếu dữ liệu để hoạt động. Cuối cùng, nếu ta muốn đồng bộ nhiều máy tính trong một mạng, kết nối mạng không nên làm chậm việc tính toán. Xen kẽ việc giao tiếp và tính toán giữa các máy tính là một phương án cho vấn đề này. Giờ hãy xem xét các thành phần trên một cách chi tiết hơn.

12.4.2. Bộ nhớ

Về cơ bản, bộ nhớ được sử dụng để lưu trữ dữ liệu khi cần sẵn sàng truy cập. Hiện tại bộ nhớ RAM của CPU thường thuộc loại DDR4, trong đó mỗi mô-đun có băng thông 20-25GB/s và độ rộng bus 64 bit. Thông thường, các cặp mô-đun bộ nhớ cho phép sử dụng đa kênh. CPU có từ 2 đến 4 kênh bộ nhớ, nghĩa là chúng có băng thông bộ nhớ tối đa từ 40 GB/s đến 100 GB/s. Thường thì mỗi kênh có hai dải (bank). Ví dụ, Zen 3 Threadripper của AMD có 8 khe cắm.

Dù những con số trên trông khá ấn tượng, trên thực tế chúng chỉ nói lên một phần nào đó. Khi muốn đọc một phần nào đó từ bộ nhớ, trước tiên ta cần chỉ cho mô-đun bộ nhớ vị trí chứa thông tin, tức cần gửi địa chỉ đến RAM. Khi thực hiện xong việc này, ta có thể chọn chỉ đọc một bản ghi 64 bit hoặc một chuỗi dài các bản ghi. Lựa chọn thứ hai được gọi là đọc nhanh (burst read). Nói ngắn gọn, việc gửi một địa chỉ vào bộ nhớ và thiết lập chuyển tiếp sẽ mất khoảng 100ns (thời gian cụ thể phụ thuộc vào hệ số thời gian của từng chip bộ nhớ được sử dụng), mỗi lần chuyển tiếp sau đó chỉ mất 0.2ns. Có thể thấy lần đọc đầu tiên tốn thời gian gấp 500 lần những lần sau! Ta có thể đọc ngẫu nhiên tối đa \(10,000,000\) lần mỗi giây. Điều này cho thấy rằng ta nên hạn chế tối đa việc truy cập bộ nhớ ngẫu nhiên và thay vào đó nên sử dụng cách đọc (và ghi) nhanh (burst read, và burst write).

Mọi thứ trở nên phức tạp hơn một chút khi ta tính đến việc có nhiều dải bộ nhớ. Mỗi dải có thể đọc bộ nhớ gần như là độc lập với nhau. Điều này có hai ý sau. Thứ nhất, số lần đọc ngẫu nhiên thực sự cao hơn tới 4 lần, miễn là chúng được trải đều trên bộ nhớ. Điều đó cũng có nghĩa là việc thực hiện các lệnh đọc ngẫu nhiên vẫn không phải là một ý hay vì các lệnh đọc nhanh (burst read) cũng nhanh hơn gấp 4 lần. Thứ hai, do việc căn chỉnh bộ nhớ theo biên 64 bit, ta nên căn chỉnh mọi cấu trúc dữ liệu theo cùng biên đó. Trình biên dịch thực hiện việc này một cách tự động khi các cờ thích hợp được đặt. Độc giả có thể tham khảo thêm bài giảng về DRAM ví dụ như Zeshan Chishti.

Bộ nhớ GPU còn yêu cầu băng thông cao hơn nữa vì chúng có nhiều phần tử xử lý hơn CPU. Nhìn chung có hai phương án tiếp cận đối với vấn đề này. Một cách là mở rộng bus bộ nhớ. Chẳng hạn NVIDIA’s RTX 2080 Ti dùng bus có kích thước 352 bit. Điều này cho phép truyền đi lượng thông tin lớn hơn cùng lúc. Một cách khác là sử dụng loại bộ nhớ chuyên biệt có hiệu năng cao cho GPU. Các thiết bị hạng phổ thông, điển hình như dòng RTX và Titan của NVIDIA, dùng các chip GDDR6 với băng thông tổng hợp hơn 500 GB/s. Một loại bộ nhớ chuyên biệt khác là mô-đun HBM (bộ nhớ băng thông rộng). Chúng dùng phương thức giao tiếp rất khác và kết nối trực tiếp với GPU trên một tấm bán dẫn silic chuyên biệt. Điều này dẫn đến giá thành rất cao và chúng chỉ được sử dụng chủ yếu cho các chip máy chủ cao cấp, ví dụ như dòng GPU NVIDIA Volta V100. Không quá ngạc nhiên, kích thước bộ nhớ GPU nhỏ hơn nhiều so với bộ nhớ CPU do giá thành cao của nó. Nhìn chung các đặc tính hiệu năng của bộ nhớ GPU khá giống bộ nhớ CPU, nhưng nhanh hơn nhiều. Ta có thể bỏ qua các chi tiết sâu hơn trong cuốn sách này, do chúng chỉ quan trọng khi cần điều chỉnh các hạt nhân GPU để đạt thông lượng xử lý cao hơn.

12.4.3. Lưu trữ

Chúng ta đã thấy đặc tính then chốt của RAM chính là băng thôngđộ trễ. Điều này cũng đúng đối với các thiết bị lưu trữ, sự khác biệt chỉ có thể là các đặc tính trên lớn hơn nhiều lần.

Các ổ cứng đã được sử dụng hơn nửa thế kỷ. Một cách ngắn gọn, chúng chứa một số đĩa quay với những đầu kim có thể di chuyển để đọc/ghi ở bất cứ rãnh nào. Các ổ đĩa cao cấp có thể lưu trữ lên tới 16 TB trên 9 đĩa. Một trong những lợi ích chính của ổ đĩa cứng là chúng tương đối rẻ. Nhược điểm của chúng là độ trễ tương đối cao khi đọc dữ liệu và hay bị hư hỏng nặng dẫn đến không thể đọc dữ liệu, thậm chí là mất dữ liệu.

Để hiểu về nhược điểm thứ hai, hãy xem xét thực tế rằng ổ cứng quay với tốc độ khoảng 7,200 vòng/phút. Nếu tốc độ này cao hơn, các đĩa sẽ vỡ tan do tác dụng của lực ly tâm. Điều này dẫn đến một nhược điểm lớn khi truy cập vào một khu vực cụ thể trên đĩa: chúng ta cần đợi cho đến khi đĩa quay đúng vị trí (chúng ta có thể di chuyển đầu kim nhưng không được tăng tốc các đĩa). Do đó, có thể mất hơn 8ms cho đến khi truy cập được dữ liệu yêu cầu. Vì thế mà ta hay nói ổ cứng có thể hoạt động ở mức xấp xỉ 100 IOP. Con số này về cơ bản vẫn không thay đổi trong hai thập kỷ qua. Tệ hơn nữa, việc tăng băng thông cũng khó khăn không kém (ở mức độ 100-200 MB/s). Rốt cuộc, mỗi đầu đọc một rãnh bit, do đó tốc độ bit chỉ tăng theo tỷ lệ căn bậc hai của mật độ thông tin. Kết quả là các ổ cứng đang nhanh chóng biến thành nơi lưu trữ cấp thấp cho các bộ dữ liệu rất lớn.

Ổ cứng thể rắn (SSD) sử dụng bộ nhớ Flash để liên tục lưu trữ thông tin. Điều này cho phép truy cập nhanh hơn nhiều vào các bản ghi đã được lưu trữ. SSD hiện đại có thể hoạt động ở mức 100,000 đến 500,000 IOP, tức là nhanh hơn gấp 1000 lần so với ổ cứng HDD. Hơn nữa, băng thông của chúng có thể đạt tới 1-3GB/s nghĩa là nhanh hơn 10 lần so với ổ cứng. Những cải tiến này nghe có vẻ tốt đến mức khó tin. Thật vậy, và SSD cũng đi kèm với một số hạn chế do cách mà chúng được thiết kế.

  • Các ổ SSD lưu trữ thông tin theo khối (256 KB trở lên). Ta sẽ phải ghi cả khối cùng một lúc, mất thêm thời gian đáng kể. Do đó việc ghi ngẫu nhiên theo bit trên SSD có hiệu suất rất tệ. Tương tự như vậy, việc ghi dữ liệu nói chung mất thời gian đáng kể vì khối phải được đọc, xóa và sau đó viết lại với thông tin mới. Cho đến nay, bộ điều khiển và firmware của SSD đã phát triển các thuật toán để giảm thiểu vấn đề này. Tuy nhiên tốc độ ghi vẫn có thể chậm hơn nhiều, đặc biệt là đối với SSD QLC (ô bốn cấp). Chìa khóa để cải thiện hiệu suất là đưa các thao tác vào một hàng đợi để ưu tiên việc đọc trước và chỉ ghi theo các khối lớn nếu có thể.
  • Các ô nhớ trong SSD bị hao mòn tương đối nhanh (thường sau vài nghìn lần ghi). Các thuật toán bảo vệ mức hao mòn có thể phân bổ đều sự xuống cấp trên nhiều ô. Dù vậy, vẫn không nên sử dụng SSD cho các tệp hoán đổi (swap file) hoặc cho tập hợp lớn các tệp nhật ký (log file).
  • Cuối cùng, sự gia tăng lớn về băng thông đã buộc các nhà thiết kế máy tính phải gắn SSD trực tiếp vào bus PCIe. Các ổ đĩa có khả năng xử lý việc này, được gọi là NVMe (Bộ nhớ không biến động tăng cường - Non Volatile Memory enhanced), có thể sử dụng lên tới 4 làn PCIe. Băng thông có thể lên tới 8GB/s trên PCIe 4.0.

Lưu trữ đám mây cung cấp nhiều lựa chọn hiệu suất có thể tùy chỉnh. Nghĩa là, việc chỉ định bộ lưu trữ cho các máy ảo là tùy chỉnh, cả về số lượng và tốc độ, do người dùng quyết định. Chúng tôi khuyên người dùng nên tăng số lượng IOP được cung cấp bất cứ khi nào độ trễ quá cao, ví dụ như trong quá trình huấn luyện với dữ liệu gồm nhiều bản ghi nhỏ.

12.4.4. CPU

Bộ xử lý trung tâm (Central Processing Units - CPU) là trung tâm của mọi máy tính (như ở phần trước, chúng tôi đã mô tả tổng quan về những phần cứng quan trọng cho các mô hình học sâu hiệu quả). CPU gồm một số thành tố quan trọng: lõi xử lý (core) với khả năng thực thi mã máy,
bus kết nối các lõi (cấu trúc kết nối cụ thể có sự khác biệt lớn giữa các mô hình xử lý, đời chip và nhà sản xuất) và bộ nhớ đệm (cache) cho phép truy cập với băng thông cao hơn và độ trễ thấp hơn so với việc đọc từ bộ nhớ chính. Cuối cùng, hầu hết CPU hiện đại chứa những đơn vị xử lý vector để hỗ trợ tính toán đại số tuyến tính và tích chập với tốc độ cao vì chúng khá phổ biến trong xử lý phương tiện và học máy.
../_images/skylake.svg

Fig. 12.4.3 CPU lõi tứ của bộ xử lý Intel Skylake

Fig. 12.4.3 minh hoạ bộ xử lý Intel Skylake với CPU lõi tứ. Nó có một GPU tích hợp, bộ nhớ cache và phương tiện kết nối bốn lõi. Thiết bị ngoại vi (Ethernet, WiFi, Bluetooth, bộ điều khiển SSD, USB, v.v.) là một phần của chipset hoặc được đính kèm trực tiếp (PCIe) với CPU.

12.4.5. Vi kiến trúc (Micro-architecture)

Mỗi nhân xử lý bao gồm các thành phần rất tinh vi. Mặc dù chi tiết khác nhau giữa đời chip và nhà sản xuất, chức năng cơ bản của chúng đã được chuẩn hóa tương đối. Front-end tải các lệnh và dự đoán nhánh nào sẽ được thực hiện (ví dụ: cho luồng điều khiển). Sau đó các lệnh được giải mã từ mã nguồn hợp ngữ (assembly code) thành vi lệnh. Mã nguồn hợp ngữ thường chưa phải là mã nguồn cấp thấp nhất mà bộ xử lý thực thi. Thay vào đó, các lệnh phức tạp có thể được giải mã thành một tập hợp các phép tính cấp thấp hơn. Tiếp đó chúng được xử lý bằng một lõi thực thi. Các bộ xử lý đời mới thường có khả năng thực hiện đồng thời nhiều câu lệnh. Ví dụ, lõi ARM Cortex A77 trong Fig. 12.4.4 có thể thực hiện lên đến 8 phép tính cùng một lúc.

../_images/a77.svg

Fig. 12.4.4 Tổng quan về vi kiến trúc ARM Cortex A77

Điều này có nghĩa là các chương trình hiệu quả có thể thực hiện nhiều hơn một lệnh trên một chu kỳ xung nhịp, giả sử rằng chúng có thể được thực hiện một cách độc lập. Không phải tất cả các bộ xử lý đều được tạo ra như nhau. Một số được thiết kế chuyên biệt cho các lệnh về số nguyên, trong khi một số khác được tối ưu hóa cho việc tính toán số thực dấu phẩy động. Để tăng thông lượng, bộ xử lý cũng có thể theo đồng thời nhiều nhánh trong một lệnh rẽ nhánh và sau đó loại bỏ các kết quả của nhánh không được thực hiện. Đây là lý do vì sao đơn vị dự đoán nhánh có vai trò quan trọng (trên front-end), bởi chúng chỉ chọn những nhánh có khả năng cao được rẽ.

12.4.6. Vector hóa (Vectorization)

Học sâu đòi hỏi sức mạnh tính toán cực kỳ lớn. Vì vậy, CPU phù hợp với học máy cần phải thực hiện được nhiều thao tác trong một chu kỳ xung nhịp. Ta có thể đạt được điều này thông qua các đơn vị vector. Trên chip ARM chúng được gọi là NEON, trên x86 thế hệ đơn vị vector mới nhất được gọi là AVX2. Một khía cạnh chung là chúng có thể thực hiện SIMD (đơn lệnh đa dữ liệu - single instruction multiple data). Fig. 12.4.5 cho thấy cách cộng 8 số nguyên ngắn trong một chu kỳ xung nhịp trên ARM.

../_images/neon128.svg

Fig. 12.4.5 Vector hóa NEON 128 bit

Phụ thuộc vào các lựa chọn kiến trúc, các thanh ghi như vậy có thể dài tới 512 bit, cho phép tổ hợp tối đa 64 cặp số. Chẳng hạn, ta có thể nhân hai số và cộng chúng với số thứ ba, cách này còn được biết đến như phép nhân-cộng hợp nhất (fused multiply-add). OpenVino của Intel sử dụng thao tác này để đạt được thông lượng đáng nể cho học sâu trên CPU máy chủ. Tuy nhiên, xin lưu ý rằng tốc độ này hoàn toàn không đáng kể so với khả năng của GPU. Ví dụ, RTX 2080 Ti của NVIDIA có 4,352 nhân CUDA, mỗi nhân có khả năng xử lý một phép tính như vậy tại bất cứ thời điểm nào.

12.4.7. Bộ nhớ đệm

Xét tình huống sau: ta có một CPU bình thường với 4 nhân như trong Fig. 12.4.3 trên, hoạt động ở tần số 2 GHz. Thêm nữa, hãy giả sử IPC (instruction per clock - số lệnh mỗi xung nhịp) là 1 và mỗi nhân đều đã kích hoạt AVX2 rộng 256 bit. Ngoài ra, giả sử bộ nhớ cần truy cập ít nhất một thanh ghi được sử dụng trong các lệnh AVX2. Điều này có nghĩa CPU xử lý 4 x 256 bit = 1 kbit dữ liệu mỗi chu kỳ xung nhịp. Trừ khi ta có thể truyền \(2 \cdot 10^9 \cdot 128 = 256 \cdot 10^9\) byte đến vi xử lý mỗi giây, các nhân sẽ thiếu dữ liệu để xử lý. Tiếc thay giao diện bộ nhớ của bộ vi xử lý như trên chỉ hỗ trợ tốc độ truyền dữ liệu khoảng 20-40 GB/s, nghĩa là thấp hơn 10 lần. Để khắc phục vấn đề này, ta cần tránh nạp dữ liệu mới từ bộ nhớ ngoài, và tốt hơn hết là lưu trong bộ nhớ cục bộ trên CPU. Đây chính là lúc bộ nhớ đệm trở nên hữu ích (xem bài viết trên Wikipedia này để bắt đầu). Một số tên gọi/khái niệm thường gặp:

  • Thanh ghi không phải là một bộ phận của bộ nhớ đệm. Chúng hỗ trợ sắp xếp các câu lệnh cho CPU. Nhưng dù sao thanh ghi cũng là một vùng nhớ mà CPU có thể truy cập với tốc độ xung nhịp mà không có độ trễ. Các CPU thường có hàng chục thanh ghi. Việc sử dụng các thanh ghi sao cho hiệu quả hoàn toàn phụ thuộc vào trình biên dịch (hoặc lập trình viên). Ví dụ như trong ngôn ngữ C, ta có thể sử dụng từ khóa register để lưu các biến vào thanh ghi thay vì bộ nhớ.
  • Bộ nhớ đệm L1 là lớp bảo vệ đầu tiên khi nhu cầu băng thông bộ nhớ quá cao. Bộ nhớ đệm L1 rất nhỏ (kích thước điển hình khoảng 32-64 kB) và thường được chia thành bộ nhớ đệm dữ liệu và câu lệnh. Nếu dữ liệu được tìm thấy trong bộ nhớ đệm L1, việc truy cập diễn ra rất nhanh chóng. Nếu không, việc tìm kiếm sẽ tiếp tục theo hệ thống phân cấp bộ nhớ đệm (cache hierarchy).
  • Bộ nhớ đệm L2 là điểm dừng tiếp theo. Vùng nhớ này có thể chuyên biệt tuỳ theo kiến trúc thiết kế và kích thước vi xử lý. Nó có thể chỉ được truy cập từ một lõi nhất định hoặc được chia sẻ với nhiều lõi khác nhau. Bộ nhớ đệm L2 có kích thước lớn hơn (thường là 256-512 kB mỗi lõi) và chậm hơn L1. Hơn nữa, để truy cập vào dữ liệu trong L2, đầu tiên ta cần kiểm tra để chắc rằng dữ liệu đó không nằm trong L1, việc này làm tăng độ trễ lên một chút.
  • Bộ nhớ đệm L3 được sử dụng chung cho nhiều lõi khác nhau và có thể khá lớn. CPU máy chủ Epyc 3 của AMD có bộ nhớ đệm 256MB cực lớn được phân bổ trên nhiều vi xử lý con (chiplet). Thường thì kích thước của L3 nằm trong khoảng 4-8MB.

Việc dự đoán phần tử bộ nhớ nào sẽ cần tiếp theo là một trong những tham số tối ưu chính trong thiết kế vi xử lý. Ví dụ, việc duyệt xuôi bộ nhớ được coi là thích hợp do đa số các thuật toán ghi đệm (caching algorithms) sẽ cố gắng đọc về trước hơn là về sau. Tương tự, việc giữ hành vi truy cập bộ nhớ ở mức cục bộ là một cách tốt để cải thiện hiệu năng. Tăng số lượng bộ nhớ đệm là một con dao hai lưỡi. Một mặt việc này đảm bảo các nhân vi xử lý không bị thiếu dữ liệu. Mặt khác nó tăng kích thước vi xử lý, lấn chiếm phần diện tích mà đáng ra có thể được sử dụng vào việc tăng khả năng xử lý. Xét trường hợp tệ nhất như mô tả trong Fig. 12.4.6. Một địa chỉ bộ nhớ được lưu trữ tại vi xử lý 0 trong khi một luồng của vi xử lý 1 yêu cầu dữ liệu đó. Để có thể lấy dữ liệu, vi xử lý 0 phải dừng công việc đang thực hiện, ghi lại thông tin vào bộ nhớ chính để vi xử lý 1 đọc dữ liệu từ đó. Trong suốt quá trình này, cả hai vi xử lý đều ở trong trạng thái chờ. Một đoạn mã như vậy khả năng cao là sẽ chạy chậm hơn trên một hệ đa vi xử lý so với một vi xử lý đơn được lập trình hiệu quả. Đây là một lý do nữa cho việc tại sao thực tế phải giới hạn kích thước bộ nhớ đệm (ngoài việc chiếm diện tích vật lý).

../_images/falsesharing.svg

Fig. 12.4.6 Chia sẻ dữ liệu lỗi (hình ảnh được sự cho phép của Intel)

12.4.8. GPU và các Thiết bị Tăng tốc khác

Không hề phóng đại khi nói rằng học sâu có lẽ sẽ không thành công nếu không có GPU. Và cũng nhờ có học sâu mà tài sản của các công ty sản suất GPU tăng trưởng đáng kể. Sự đồng tiến hóa giữa phần cứng và các thuật toán dẫn tới tình huống mà học sâu trở thành mẫu mô hình thống kê được ưa thích bất kể có hiệu quả hay không. Do đó, ta cần phải hiểu rõ ràng lợi ích mà GPU và các thiết bị tăng tốc khác như TPU [Jouppi et al., 2017] mang lại.

Ta cần chú ý đến đặc thù thường được sử dụng trong thực tế: thiết bị tăng tốc được tối ưu hoặc cho bước huấn luyện hoặc cho bước suy luận. Đối với bước suy luận, ta chỉ cần tính toán lượt truyền xuôi qua mạng, không cần sử dụng bộ nhớ để lưu dữ liệu trung gian ở bước lan truyền ngược. Hơn nữa, ta có thể không cần đến phép tính quá chính xác (thường thì FP16 hoặc INT8 là đủ) Mặt khác trong quá trình huấn luyện, tất cả kết quả trung gian đều cần phải lưu lại để tính gradient. Hơn nữa, việc tích luỹ gradient yêu cầu độ chính xác cao hơn nhằm tránh lỗi tràn số trên hoặc dưới, do đó bước huấn luyện yêu cầu tối thiểu độ chính xác FP16 (hoặc độ chính xác hỗn hợp khi kết hợp với FP32). Tất cả các yếu tố trên đòi hỏi bộ nhớ nhanh hơn và lớn hơn (HBM2 hoặc GDDR6) và nhiều khả năng xử lý hơn. Ví dụ, GPU Turing T4 của NVIDIA được tối ưu cho bước suy luận trong khi GPU V100 phù hợp cho quá trình huấn luyện.

Xem lại Fig. 12.4.5. Việc thêm các đơn vị vector vào lõi vi xử lý cho phép ta tăng đáng kể thông lượng xử lý (ở ví dụ trong hình ta có thể thực hiện 16 thao tác cùng lúc). Chuyện gì sẽ xảy ra nếu ta không chỉ tối ưu cho phép tính giữa các vector mà còn tối ưu cho các ma trận? Chiến lược này dẫn tới sự ra đời của Lõi Tensor (chi tiết sẽ được thảo luận sau đây). Thứ hai, nếu tăng số lượng lõi thì sao? Nói tóm lại, hai chiến lược trên tóm tắt việc quyết định thiết kế của GPU. Fig. 12.4.7 mô tả tổng quan một khối xử lý đơn giản, bao gồm 16 đơn vị số nguyên và 16 đơn vị dấu phẩy động. Thêm vào đó, hai Lõi Tensor xử lý một tập nhỏ các thao thác liên quan đến học sâu được thêm vào. Mỗi Hệ vi xử lý Luồng (Streaming Multiprocessor - SM) bao gồm bốn khối như vậy.

../_images/turing_processing_block.png

Fig. 12.4.7 Khối Xử lý Turing của NVIDIA

12 hệ vi xử lý luồng sau đó được nhóm vào một cụm xử lý đồ hoạ tạo nên vi xử lý cao cấp TU102. Số lượng kênh bộ nhớ phong phú và bộ nhớ đệm L2 được bổ sung vào cấu trúc. Thông tin chi tiết được mô tả trong Fig. 12.4.8. Một trong những lý do để thiết kế một thiết bị như vậy là từng khối riêng biệt có thể được thêm vào hoặc bỏ đi tuỳ theo nhu cầu để có thể tạo thành một vi xử lý nhỏ gọn và giải quyết một số vấn đề phát sinh (các mô-đun lỗi có thể không được kích hoạt). May mắn thay, các nhà nghiên cứu học sâu bình thường không cần lập trình cho các thiết bị này do đã có các lớp mã nguồn framework CUDA ở tầng thấp. Cụ thể, có thể có nhiều hơn một chương trình được thực thi đồng thời trên GPU, với điều kiện là còn đủ tài nguyên. Tuy nhiên ta cũng cần để ý đến giới hạn của các thiết bị nhằm tránh việc lựa chọn mô hình quá lớn so với bộ nhớ của thiết bị.

../_images/turing.png

Fig. 12.4.8 Kiến trúc Turing của NVIDIA (hình ảnh được sự cho phép của NVIDIA)

Khía cạnh cuối cùng đáng để bàn luận chi tiết là Lõi Tensor (TensorCore). Đây là một ví dụ của xu hướng gần đây là sử dụng thêm nhiều mạch đã được tối ưu để tăng hiệu năng cho học sâu. Ví dụ, TPU có thêm một mảng tâm thu (systolic array) [Kung, 1988] để tăng tốc độ nhân ma trận. Thiết kế của TPU chỉ hỗ trợ một số lượng rất ít các phép tính kích thước lớn (thế hệ TPU đầu tiên hỗ trợ một phép tính). Lõi Tensor thì ngược lại, được tối ưu cho các phép tính kích thước nhỏ cho các ma trận kích thước 4x4 đến 16x16, tuỳ vào độ chính xác số học. Fig. 12.4.9 mô tả tổng quan quá trình tối ưu.

../_images/turing.png

Fig. 12.4.9 Lõi Tensor của NVIDIA trong Turing (hình ảnh được sự cho phép của NVIDIA)

Đương nhiên khi tối ưu cho quá trình tính toán, ta buộc phải có một số đánh đổi nhất định. Một trong số đó là GPU không xử lý tốt dữ liệu ngắt quãng hoặc thưa. Trừ một số ngoại lệ đáng chú ý, ví dụ như Gunrock [Wang et al., 2016], việc truy cập vector và ma trận thưa không phù hợp với các thao tác đọc theo cụm (burst read) với băng thông cao của GPU. Đạt được cả hai mục tiêu là một lĩnh vực đang được đẩy mạnh nghiên cứu. Ví dụ, tham khảo DGL, một thư viện được điều chỉnh cho phù hợp với học sâu trên đồ thị.

12.4.9. Mạng máy tính và Bus

Mỗi khi một thiết bị đơn không đủ cho quá trình tối ưu, ta cần chuyển dữ liệu đến và đi khỏi nó để đồng bộ hóa quá trình xử lý. Đây chính là lúc mà mạng máy tính và bus trở nên hữu dụng. Ta có một vài tham số thiết kế gồm: băng thông, chi phí, khoảng cách và tính linh hoạt. Tuy ta cũng có Wifi với phạm vi hoạt động tốt, dễ dàng để sử dụng (dù sao cũng là không dây), rẻ nhưng lại có băng thông không quá tốt và độ trễ lớn. Sẽ không có bất cứ nhà nghiên cứu học máy tỉnh táo nào lại nghĩ đến việc sử dụng Wifi để xây dựng một cụm máy chủ. Sau đây, ta sẽ chỉ tập trung vào các cách kết nối phù hợp cho học sâu.

  • PCIe là một bus riêng chỉ phục vụ cho kết nối điểm – điểm với băng thông trên mỗi làn rất lớn (lên đến 16 GB/s trên PCIe 4.0). Độ trễ thường có giá trị cỡ vài micro giây (5 μs). Kết nối PCIe khá quan trọng. Vi xử lý chỉ có một số lượng làn PCIe nhất định: EPYC 3 của AMD có 128 làn, Xeon của Intel lên đến 48 làn cho mỗi chip; trên CPU dùng cho máy tính để bàn, số lượng này lần lượt là 20 (với Ryzen 9) và 16 (với Core i9). Do GPU thường có 16 luồng nên số lượng GPU có thể kết nối với CPU bị giới hạn tại băng thông tối đa. Xét cho cùng, chúng cần chia sẻ liên kết với các thiết bị ngoại vi khác như bộ nhớ và cổng Ethernet. Giống như việc truy cập RAM, việc truyền lượng lớn dữ liệu thường được ưa chuộng hơn nhằm giảm tổng chi phí theo gói tin.
  • Ethernet là cách phổ biến nhất để kết nối máy tính với nhau. Dù nó chậm hơn đáng kể so với PCIe, nó rất rẻ và dễ cài đặt, bao phủ khoảng cách lớn hơn nhiều. Băng thông đặc trưng đối với máy chủ cấp thấp là 1 GBit/s. Các thiết bị cao cấp hơn (ví dụ như máy chủ loại C5 trên AWS) cung cấp băng thông từ 10 đến 100 GBit/s. Cũng như các trường hợp trên, việc truyền dữ liệu có tổng chi phí đáng kể. Chú ý rằng ta hầu như không bao giờ sử dụng trực tiếp Ethernet thuần mà sử dụng một giao thức được thực thi ở tầng trên của kết nối vật lý (ví dụ như UDP hay TCP/IP). Việc này làm tăng tổng chi phí. Giống như PCIe, Ethernet được thiết kế để kết nối hai thiết bị, ví dụ như máy tính với một bộ chuyển mạch (switch).
  • Bộ chuyển mạch cho phép ta kết nối nhiều thiết bị theo cách mà bất cứ cặp thiết bị nào cũng có thể (thường là với băng thông tối đa) thực hiện kết nối điểm – điểm cùng lúc. Ví dụ, bộ chuyển mạch Ethernet có thể kết nối 40 máy chủ với băng thông xuyên vùng (cross-sectional bandwidth) cao. Chú ý rằng bộ chuyển mạch không phải chỉ có trong mạng máy tính truyền thống. Ngay cả làn PCIe cũng có thể chuyển mạch. Điều này xảy ra khi kết nối một lượng lớn GPU tới vi xử lý chính, như với trường hợp máy chủ loại P2.
  • NVLink là một phương pháp thay thế PCIe khi ta cần kết nối với băng thông rất lớn. NVLink cung cấp tốc độ truyền dữ liệu lên đến 300 Gbit/s mỗi đường dẫn (link). GPU máy chủ (Volta V100) có 6 đường dẫn, trong khi GPU thông dụng (RTX 2080 Ti) chỉ có một đường dẫn, hoạt động ở tốc độ thấp 100 Gbit/s. Vì vậy, chúng tôi gợi ý sử dụng NCCL để có thể đạt được tốc độ truyền dữ liệu cao giữa các GPU.

12.4.10. Tóm tắt

  • Các thiết bị đều có chi phí phụ trợ trên mỗi hành động. Do đó ta nên nhắm tới việc di chuyển ít lần các lượng dữ liệu lớn thay vì di chuyển nhiều lần các lượng dữ liệu nhỏ. Điều này đúng với RAM, SSD, các thiết bị mạng và GPU.
  • Vector hóa rất quan trọng để tăng hiệu năng. Hãy đảm bảo bạn hiểu các điểm mạnh đặc thù của thiết bị tăng tốc mình đang có. Ví dụ, một vài CPU Intel Xeon thực hiện cực kỳ hiệu quả phép toán với dữ liệu kiểu INT8, GPU NVIDIA Volta rất phù hợp với các phép toán với ma trận dữ liệu kiểu FP16; còn NVIDIA Turing chạy tốt cho cả các phép toán với dữ liệu kiểu FP16, INT8, INT4.
  • Hiện tượng tràn số trên do kiểu dữ liệu không đủ số bit để biểu diễn giá trị có thể là một vấn đề khi huấn luyện (và cả khi suy luận, dù ít nghiêm trọng hơn).
  • Việc cùng dữ liệu nhưng có nhiều địa chỉ (aliasing) có thể làm giảm đáng kể hiệu năng. Ví dụ, việc sắp xếp dữ liệu trong bộ nhớ (memory alignment) trên CPU 64 bit nên được thực hiện theo từng khối 64 bit. Trên GPU, tốt hơn là nên giữ kích thước tích chập đồng bộ, với TensorCores chẳng hạn.
  • Sử dụng thuật toán phù hợp với phần cứng (về mức chiếm dụng bộ nhớ, băng thông, v.v). Thời gian thực thi có thể giảm hàng trăm ngàn lần khi tất cả tham số đều được chứa trong bộ đệm.
  • Chúng tôi khuyến khích bạn đọc tính toán trước hiệu năng của một thuật toán mới trước khi kiểm tra bằng thực nghiệm. Sự khác biệt lên tới hàng chục lần hoặc hơn là dấu hiệu cần quan tâm.
  • Sử dụng các công cụ phân tích hiệu năng (profiler) để tìm điểm nghẽn cổ chai của hệ thống.
  • Phần cứng sử dụng cho huấn luyện và suy luận có các cấu hình hiệu quả khác nhau để cân đối giá tiền và hiệu năng.

12.4.11. Độ trễ

Các thông tin trong table_latency_numberstable_latency_numbers_tesla được Eliot Eshelman duy trì cập nhật trên GitHub Gist.

:Các độ trễ thường gặp.

Table 12.4.1 label:table_latency_numbers
Hoạt động Th ời gi an Chú thích
Truy xuất bộ đệm L1 1. 5 ns 4 chu kỳ
Cộng, nhân, cộng kết hợp nhân (FMA) số thực dấu phẩy động 1. 5 ns 4 chu kỳ
Truy xuất bộ đệm L2 5 ns 12 ~ 17 chu kỳ
Rẽ nhánh sai 6 ns 15 ~ 20 chu kỳ
Truy xuất bộ đệm L3 (không chia sẻ) 16 ns 42 chu kỳ
Truy xuất bộ đệm L3 (chia sẻ với nhân khác) 25 ns 65 chu kỳ
Khóa/mở đèn báo lập trình (mutex) 25 ns  
Truy xuất bộ đệm L3 (được nhân khác thay đổi) 29 ns 75 chu kỳ
Truy xuất bộ đệm L3 (tại CPU socket từ xa) 40 ns 100 ~ 300 chu kỳ (40 ~ 116 ns)
QPI hop đến CPU khác (cho mỗi hop) 40 ns  
Truy xuất 64MB (CPU cục bộ) 46 ns TinyMemBench trên Broadwell E5-2690v4
Truy xuất 64MB (CPU từ xa) 70 ns TinyMemBench trên Broadwell E5-2690v4
Truy xuất 256MB (CPU cục bộ) 75 ns TinyMemBench trên Broadwell E5-2690v4
Ghi ngẫu nhiên vào Intel Optane 94 ns UCSD Non-Volatile Systems Lab
Truy xuất 256MB (CPU từ xa) 12 0 ns TinyMemBench trên Broadwell E5-2690v4
Đọc ngẫu nhiên từ Intel Optane 30 5 ns UCSD Non-Volatile Systems Lab
Truyền 4KB trên sợi HPC 100 Gbps 1 μs MVAPICH2 trên Intel Omni-Path
Nén 1KB với Google Snappy 3 μs  
Truyền 4KB trên cáp mạng 10 Gbps 10 μs  
Ghi ngẫu nhiên 4KB vào SSD NVMe 30 μs DC P3608 SSD NVMe (QOS 99% khoảng 500μs)
Truyền 1MB từ/đến NVLink GPU 30 μs ~33GB/s trên NVIDIA 40GB NVLink
Truyền 1MB từ/đến PCI-E GPU 80 μs ~12GB/s trên PCIe 3.0 x16 link
Đọc ngẫu nhiên 4KB từ SSD NVMe 12 0 μs DC P3608 SSD NVMe (QOS 99%)
Đọc tuần tự 1MB từ SSD NVMe 20 8 μs ~4.8GB/s DC P3608 SSD NVMe
Ghi ngẫu nhiên 4KB vào SSD SATA 50 0 μs DC S3510 SSD SATA (QOS 99.9%)
Đọc ngẫu nhiên 4KB từ SSD SATA 50 0 μs DC S3510 SSD SATA (QOS 99.9%)
Truyền 2 chiều trong cùng trung tâm dữ liệu 50 0 μs Ping một chiều ~250μs
Đọc tuần tự 1MB từ SSD SATA 2 ms ~550MB/s DC S3510 SSD SATA
Đọc tuần tự 1MB từ ổ đĩa 5 ms ~200MB/s server HDD
Truy cập ngẫu nhiên ổ đĩa (tìm + xoay) 10 ms  
Gửi gói dữ liệu từ California -> Hà Lan -> California 15 0 ms  

:Độ trễ của GPU NVIDIA Tesla.

Table 12.4.2 label:table_latency_numbers_tesla
Hoạt động Thời gian Chú thích
Truy cập bộ nhớ chung của GPU 30 ns 30~90 chu kỳ (tính cả xung đột của các bank)
Truy cập bộ nhớ toàn cục của GPU 200 ns 200~800 chu kỳ
Khởi chạy nhân CUDA trên GPU 10 μs CPU host ra lệnh cho GPU khởi chạy nhân
Truyền 1MB từ/đến GPU NVLink 30 μs ~33GB/s trên NVIDIA NVLink 40GB
Truyền 1MB từ/đến GPU PCI-E 80 μs ~12GB/s trên PCI-Express link x16

12.4.12. Bài tập

  1. Viết đoạn mã C để so sánh tốc độ khi truy cập bộ nhớ được sắp xếp theo khối (aligned memory) với khi truy cập bộ nhớ không được sắp xếp như vậy (một cách tương đối so với bộ nhớ ngoài). Gợi ý: hãy loại bỏ hiệu ứng của bộ nhớ đệm.
  2. So sánh tốc độ khi truy cập bộ nhớ tuần tự với khi truy cập theo sải bước cho trước.
  3. Làm thế nào để đo kích thước bộ nhớ đệm trên CPU?
  4. Bạn sẽ sắp xếp dữ liệu trên nhiều bộ nhớ như thế nào để có băng thông tối đa? Sắp xếp như thế nào nếu bạn có nhiều luồng nhỏ?
  5. Tốc độ quay của một ổ cứng HDD dùng cho công nghiệp là 10,000 rpm. Thời gian tối thiểu mà HDD đó cần (trong trường hợp tệ nhất) trước khi có thể đọc dữ liệu là bao nhiêu (có thể giả sử các đầu đọc ổ đĩa di chuyển tức thời)?
  6. Giả sử nhà sản xuất HDD tăng sức chứa bộ nhớ từ 1 Tbit mỗi inch vuông lên 5 Tbit mỗi inch vuông. Có thể lưu bao nhiêu dữ liệu trên một đĩa từ của một HDD 2.5”? Có sự khác biệt nào giữa track trong và track ngoài không?
  7. Một máy chủ loại P2 trên AWS có 16 GPU K80 Kepler. Sử dụng lệnh lspci trên một máy p2.16xlarge và một máy p2.8xlarge để hiểu cách các GPU được kết nối với các CPU. Gợi ý: để ý đến chip cầu nối PLX cho chuẩn kết nối PCI.
  8. Chuyển từ kiểu dữ liệu 8 bit sang 16 bit cần lượng silicon gấp 4 lần. Tại sao? Tại sao NVIDIA thêm các phép toán cho kiểu dữ liệu INT4 vào GPU Turing?
  9. Có 6 đường truyền tốc độ cao giữa các GPU (như GPU Volta V100 chẳng hạn), bạn sẽ kết nối 8 GPU đó như thế nào? Tham khảo cách kết nối cho máy chủ p3.16xlarge trên AWS.
  10. Đọc xuôi bộ nhớ nhanh gấp bao nhiêu lần đọc ngược? Sự chênh lệch này có khác nhau giữa các nhà sản xuất máy tính và CPU không? Tại sao? Thí nghiệm với mã nguồn C.
  11. Bạn có thể đo kích thước bộ nhớ đệm trên ổ đĩa của mình không? Bộ nhớ đệm trên HDD là gì? SSD có cần bộ nhớ đệm không?
  12. Chi phí bộ nhớ phụ trợ khi gửi một gói dữ liệu qua cáp mạng (Ethernet) là bao nhiêu. So sánh các giao thức UDP và TCP/IP.
  13. Truy cập Bộ nhớ Trực tiếp (Direct Memory Access) cho phép các thiết bị khác ngoài CPU ghi (và đọc) trực tiếp vào (từ) bộ nhớ. Tại sao đây là một ý tưởng hay?
  14. Nhìn vào thông số hiệu năng của GPU Turing T4. Tại sao hiệu năng chỉ tăng gấp đôi khi chuyển từ phép toán với kiểu dữ liệu FP16 sang INT8 và INT4?
  15. Thời gian truyền một gói dữ liệu hai chiều giữa San Francisco và Amsterdam là bao nhiêu? Gợi ý: giả sử khoảng cách giữa 2 thành phố là 10,000km.

12.4.13. Thảo luận

12.4.14. Những người thực hiện

Bản dịch trong trang này được thực hiện bởi:

  • Đoàn Võ Duy Thanh
  • Nguyễn Văn Quang
  • Phạm Minh Đức
  • Lê Khắc Hồng Phúc
  • Nguyễn Văn Cường
  • Nguyễn Mai Hoàng Long
  • Trần Yến Thy
  • Nguyễn Thanh Hòa
  • Đỗ Trường Giang
  • Phạm Hồng Vinh