12.7. Máy chủ Tham số

Khi ta chuyển từ các GPU đơn sang đa GPU rồi sang nhiều máy chủ đa GPU, có khả năng các GPU được dàn trải qua nhiều khay chứa và bộ chuyển mạch mạng. Điều này khiến các giải thuật huấn luyện phân tán và song song trở nên phức tạp hơn nhiều. Các chi tiết nhỏ cũng trở nên quan trọng vì các phương thức kết nối khác nhau có băng thông rất khác nhau. Chẳng hạn, NVLink có băng thông lên tới 100GB/s qua 6 đường kết nối với cách thiết lập thích hợp, PCIe 3.0 16x làn có băng thông 16GB/s, trong khi ngay cả Ethernet 100GbE tốc độ cao chỉ đạt 10GB/s. Ngoài ra, khó có thể hy vọng rằng một nhà xây dựng mô hình thống kê cũng là một chuyên gia về kết nối mạng và hệ thống.

Ý tưởng cốt lõi của máy chủ tham số được đề xuất từ [Smola & Narayanamurthy, 2010] trong ngữ cảnh các mô hình biến ẩn phân tán. Kế tiếp, một bản mô tả về ý nghĩa của tác vụ đẩy và kéo (push and pull) được giới thiệu trong [Ahmed et al., 2012] và một bản mô tả về hệ thống này cùng với thư viện mã nguồn mở được công bố trong [Li et al., 2014]. Trong phần kế tiếp, ta sẽ tìm hiểu các thành phần cần thiết để đạt được hiệu suất cao.

12.7.1. Huấn luyện Song song Dữ liệu

Hãy cùng xem xét tổng quan phương pháp huấn luyện song song dữ liệu cho việc huấn luyện phân tán. Ta bắt đầu bằng cách này vì việc lập trình sẽ trở nên đơn giản hơn nhiều so với những cách khác. Vì các GPU ngày nay có khá nhiều bộ nhớ, gần như không có một trường hợp đặc biệt nào (ngoại trừ phương pháp học sâu trên đồ thị) mà một phương pháp song song hóa khác lại thích hợp hơn. Fig. 12.7.1 mô tả biến thể của việc song song hóa dữ liệu mà ta đã lập trình ở phần trước. Khía cạnh then chốt ở dạng này là việc tổng hợp gradient diễn ra trên GPU 0 trước khi các tham số cập nhật được phân phát tới tất cả GPU.

../_images/ps.svg

Fig. 12.7.1 Trái: việc huấn luyện trên một GPU; Phải: dạng biến thể của việc huấn luyện trên nhiều GPU. Quá trình diễn ra như sau: (1) Ta tính mất mát và gradient, (2) tất cả gradient được tổng hợp trên một GPU, (3) ta cập nhật tham số và các tham số đó được phân phối lại tới tất cả GPU.

Nhìn lại, ta không có lý do gì đặc biệt khi quyết định tổng hợp gradient trên GPU 0. Dù sao thì ta cũng có thể tổng hợp gradient trên CPU. Và ta còn có thể tổng hợp một vài tham số trên một GPU và các tham số còn lại trên một GPU khác. Miễn là thuật toán tối ưu hỗ trợ điều này, ta không có lý do gì để không thể thực hiện. Ví dụ, giả sử ta có bốn vector tham số \(\mathbf{v}_1, \ldots, \mathbf{v}_4\) với các gradient tương ứng là \(\mathbf{g}_1, \ldots, \mathbf{g}_4\), ta có thể tổng hợp gradient của mỗi vector tham số trên một GPU.

(12.7.1)\[\mathbf{g}_{i} = \sum_{j \in \mathrm{GPU}} \mathbf{g}_{ij}\]

Cách lý luận này trông có vẻ rất tùy tiện và vô nghĩa. Sau cùng, phần toán xuyên suốt bên dưới vẫn không thay đổi. Nhưng ở đây chúng ta đang làm việc cùng các thiết bị phần cứng vật lý với các bus có băng thông khác nhau như đã thảo luận ở Section 12.4. Xét một máy chủ GPU 4-chiều được mô tả trong Fig. 12.7.2. Nếu nó được kết nối cực kỳ tốt, nó có thể sở hữu một card mạng với tốc độ 100 GbE. Những con số phổ biến hơn thường nằm trong khoảng 1-10 GbE với băng thông hiệu dụng từ 100MB/s đến 1GB/s. Vì các CPU thường có quá ít làn PCIe để kết nối trực tiếp với toàn bộ GPU (ví dụ, CPU thông dụng của Intel có 24 làn) ta cần một mạch đa hợp (multiplexer). Băng thông tới CPU qua cổng PCIe 16 làn thế hệ 3 là 16GB/s. Đây cũng là tốc độ mà mỗi GPU được kết nối với bộ chuyển mạch. Điều này có nghĩa là việc truyền tin trực tiếp giữa các GPU sẽ hiệu quả hơn.

../_images/bw-hierarchy.svg

Fig. 12.7.2 Một máy chủ GPU 4-chiều.

Để minh họa cho luận điểm trên, giả sử ta cần 160MB để lưu trữ các gradient. Trong trường hợp này, sẽ tốn 30ms để gửi các giá trị gradient này từ 3 thiết bị GPU đến chiếc GPU còn lại (mỗi đợt truyền tin tốn 10ms = 160MB / 16GB/s). Việc truyền lại các vector trọng số mất thêm 30ms nữa, tổng cộng tốn 60ms. Nếu ta gửi toàn bộ dữ liệu đến CPU sẽ phát sinh thêm 40ms vì mỗi GPU cần gửi dữ liệu đến CPU, và tính cả thời gian truyền lại các vector trọng số sẽ tốn 80ms. Cuối cùng, giả định rằng ta có thể chia nhỏ các giá trị gradient thành bốn phần, mỗi phần 40MB. Giờ ta có thể tổng hợp mỗi phần trên một GPU riêng biệt một cách đồng thời vì bộ chuyển mạch PCIe cho phép sử dụng toàn bộ băng thông cho mỗi kết nối. Thay vì 30ms như trước, quá trình này chỉ tốn 7.5ms và 15ms cho toàn bộ quá trình đồng bộ. Nói ngắn gọn, tùy thuộc vào cách các tham số được đồng bộ với nhau, quá trình này có thể chiếm từ 15ms đến 80ms. Fig. 12.7.3 minh họa sự khác biệt giữa các chiến lược trao đổi tham số khác nhau.

../_images/ps-distributed.svg

Fig. 12.7.3 Các chiến lược đồng bộ

Lưu ý rằng ta còn một công cụ nữa để sử dụng khi muốn cải thiện hiệu suất: trong một mạng sâu sẽ cần một khoảng thời gian để tính toán toàn bộ gradient từ trên xuống dưới. Ta có thể bắt đầu đồng bộ gradient cho một vài nhóm tham số trong khi chúng ta vẫn đang bận tính gradient cho những nhóm khác (các chi tiết kỹ thuật để thực hiện việc này khá phức tạp). Bạn đọc hãy tham khảo [Sergeev & DelBalso, 2018] để biết chi tiết cách làm điều này trong Horovod.

12.7.2. Đồng bộ dạng Vòng

Khi nói tới đồng bộ hóa trên các phần cứng học sâu tiên tiến, ta thường gặp những cách kết nối mạng rất riêng. Ví dụ, máy P3.16xlarge trên AWS và NVIDIA DGX-2 cùng sử dụng cấu trúc kết nối trong Fig. 12.7.4. Mỗi GPU kết nối với một CPU chủ thông qua kết nối PCIe có tốc độ tối đa là 16 GB/s. Hơn nữa, mỗi GPU có 6 kết nối NVLink với khả năng truyền đến 300 Gbit/s theo cả hai hướng. Điều này có nghĩa là mỗi kết nối sẽ có tốc độ khoảng 18 GB/s theo mỗi hướng. Nói ngắn gọn, băng thông tổng hợp của NVLink lớn hơn đáng kể so với băng thông của PCIe. Câu hỏi đặt ra là làm sao để tận dụng triệt để điều đó.

../_images/nvlink.svg

Fig. 12.7.4 Kết nối NVLink trên các máy chủ 8 GPU V100 (hình ảnh được sự đồng ý từ NVIDIA).

Hóa ra theo [Wang et al., 2018], chiến thuật đồng bộ tối ưu là phân tách mạng thành hai kết nối dạng vòng và sử dụng chúng để đồng bộ dữ liệu một cách trực tiếp. Fig. 12.7.5 minh họa việc mạng có thể được phân tách thành một kết nối dạng vòng (1-2-3-4-5-6-7-8-1) với băng thông NVLink gấp đôi và một kết nối dạng vòng khác (1-4-6-3-5-8-2-7-1) với băng thông bình thường. Việc thiết kế một giao thức đồng bộ hóa hiệu quả trong trường hợp này không hề đơn giản.

../_images/nvlink-twoloop.svg

Fig. 12.7.5 Phân tách mạng NVLink thành hai kết nối dạng vòng.

Xét một thí nghiệm tưởng tượng như sau: cho một kết nối dạng vòng có \(n\) đơn vị tính toán (GPU) ta có thể truyền các giá trị gradient từ thiết bị thứ nhất đến thiết bị thứ hai. Ở đó nó sẽ được cộng thêm vào gradient cục bộ và rồi truyền tiếp đến thiết bị thứ ba, và tiếp tục như vậy với các thiết bị sau. Sau \(n-1\) bước, gradient tổng hợp sẽ nằm ở thiết bị cuối cùng. Điều này có nghĩa là thời gian tổng hợp gradient sẽ tăng tuyến tính theo số lượng thiết bị trong mạng. Nhưng nếu ta làm vậy, thuật toán sẽ hoạt động kém hiệu quả. Dù sao, tại mọi thời điểm chỉ có một thiết bị thực hiện việc truyền tin. Chuyện gì sẽ xảy ra nếu ta chia các giá trị gradient thành \(n\) khúc và bắt đầu đồng bộ khúc thứ \(i\) tại thiết bị \(i\)? Vì mỗi khúc có kích thước \(1/n\), tổng thời gian giờ sẽ là \((n-1)/n \approx 1\). Nói cách khác, thời gian tổng hợp gradient không tăng khi ta tăng số thiết bị trong mạng. Quả là một kết quả đáng kinh ngạc. Fig. 12.7.6 minh họa chuỗi các bước với số thiết bị \(n=4\).

../_images/ringsync.svg

Fig. 12.7.6 Đồng bộ vòng trên 4 nút. Mỗi nút truyền một phần gradient sang nút liền kề bên trái cho đến khi gradient đầy đủ có thể được tìm thấy tại nút liền kề bên phải nó.

Nếu vẫn sử dụng ví dụ đồng bộ 160 MB trên 8 GPU V100, ta có thể đạt xấp xỉ \(2 \cdot 160 \mathrm{MB} / (3 \cdot 18 \mathrm{GB/s}) \approx 6 \mathrm{ms}\). Kết quả này tốt hơn so với việc sử dụng bus PCIe một chút, mặc dù lúc này ta sử dụng đến 8 GPU. Chú ý rằng trong thực tế những con số này sẽ không được tốt như vậy, do các framework học sâu thường gặp khó khăn trong việc tổng hợp thông tin thành cụm lớn hơn để truyền đi. Hơn nữa, việc định thời là cực kì quan trọng. Lưu ý, mọi người thường hiểu nhầm rằng đồng bộ vòng có bản chất khác hẳn so với các thuật toán đồng bộ khác. Thực ra điểm khác biệt duy nhất nằm ở đường đi đồng bộ có phần tinh vi hơn so với phương pháp cây đơn giản.

12.7.3. Huấn luyện trên Nhiều Máy tính

Việc huấn luyện phân tán trên nhiều máy tính tạo nên một thử thách mới: ta cần phải giao tiếp với các máy chủ chỉ được liên kết với nhau qua loại cáp có băng thông tương đối thấp. Trong một số trường hợp tốc độ thậm chí có thể chậm gấp hơn 10 lần. Đồng bộ nhiều thiết bị là công việc khá phức tạp. Suy cho cùng, mỗi máy tính khác nhau chạy đoạn mã huấn luyện với tốc độ khác nhau đôi chút. Do đó ta cần đồng bộ chúng nếu muốn sử dụng tối ưu phân tán đồng bộ. Fig. 12.7.7 mô tả quá trình huấn luyện phân tán song song.

  1. Một batch dữ liệu (khác nhau) được đọc trên mỗi máy tính, chia đều cho các GPU và truyền đến bộ nhớ của GPU. Ở đó các dự đoán và gradient được tính toán riêng biệt theo từng batch trên các GPU khác nhau.
  2. Các gradient trên tất cả các GPU cục bộ được tổng hợp trên một GPU (hoặc các phần khác nhau được tổng hợp trên nhiều GPU khác nhau).
  3. Các gradient được truyền đến CPU.
  4. CPU truyền các gradient đến máy chủ tham số trung tâm để tổng hợp tất cả các gradient.
  5. Các gradient tổng sau đó được sử dụng để cập nhật các vector trọng số. Tiếp đó thì các vector trọng số mới được phân phát cho các CPU.
  6. Thông tin cập nhật được truyền tới một (hoặc nhiều) GPU.
  7. Các vector trọng số đã được cập nhật sau đó được phân bố đều cho tất cả các GPU.
../_images/ps-multimachine.svg

Fig. 12.7.7 Huấn luyện song song phân tán trên nhiều máy tính đa GPU

Các thao tác trên nhìn qua thì có vẻ khá dễ hiểu. Quả thật, chúng có thể được thực hiện một cách hiệu quả trong một máy tính. Tuy nhiên khi xét trên nhiều máy tính, ta có thể thấy rằng chính máy chủ tham số trung tâm đã trở thành nút nghẽn cổ chai. Suy cho cùng, băng thông của mỗi máy chủ là có hạn, do đó đối với \(m\) máy thợ, thời gian để truyền toàn bộ gradient đến máy chủ là \(O(m)\). Ta có thể vượt qua rào cản này bằng cách tăng số lượng máy chủ lên \(n\). Khi đó mỗi máy chủ chỉ cần lưu trữ \(O(1/n)\) tham số, do đó tổng thời gian cần để cập nhật và tối ưu trở thành \(O(m/n)\). Tổng thời gian này sẽ tăng lên theo hằng số bất kể số lượng máy thợ ta sử dụng là bao nhiêu. Trong thực tế, các máy tính sẽ vừa là máy chủ và máy thợ. Fig. 12.7.8 minh hoạ thiết kế này. Độc giả có thể tham khảo [Li et al., 2014] để biết thêm chi tiết. Đặc biệt, việc đảm bảo các máy tính hoạt động với độ trễ không quá lớn không phải là một chuyện dễ dàng. Chúng tôi sẽ bỏ qua chi tiết về các rào cản và chỉ đề cập ngắn gọn tới việc cập nhật đồng bộ và bất đồng bộ dưới đây.

../_images/ps-multips.svg

Fig. 12.7.8 Trên - một máy chủ tham số là một nút nghẽn cổ chai do băng thông của nó có hạn. Dưới - nhiều máy chủ tham số lưu trữ từng phần các tham số với băng thông tổng.

12.7.4. Lưu trữ (Khóa, Giá trị)

Lập trình các bước cần thiết trên cho việc huấn luyện phân tán trên nhiều GPU trong thực tế không hề đơn giản. Cụ thể, có khả năng ta sẽ gặp rất nhiều lựa chọn khác nhau. Do đó, rất đáng để sử dụng một phép trừu tượng hóa khá phổ biến là lưu trữ cặp (khóa, giá trị) với cách cập nhật được định nghĩa lại. Trên nhiều máy chủ và nhiều GPU, việc tính toán gradient có thể được định nghĩa như sau

(12.7.2)\[\mathbf{g}_{i} = \sum_{k \in \mathrm{máy~thợ}} \sum_{j \in \mathrm{GPU}} \mathbf{g}_{ijk}.\]

Đặc điểm chính của thao tác này nằm ở việc nó là một phép rút gọn có tính giao hoán, tức nó gộp nhiều vector thành một vector và thứ tự áp dụng thao tác này không quan trọng. Vì không cần (phải) kiểm soát chi tiết thời điểm gradient được nhận, thao tác này rất phù hợp với mục đích của chúng ta. Lưu ý rằng ta có thể thực hiện phép rút gọn theo từng bước. Thêm nữa, chú ý rằng thao tác này độc lập giữa các khối \(i\) gắn liền với các tham số (và các gradient) khác nhau.

Điều này cho phép ta định nghĩa hai thao tác sau: đẩy, để cộng dồn gradient; và kéo, để lấy lại gradient được cộng dồn. Vì ta có nhiều tập gradient (do có nhiều tầng), ta cần gán chỉ số cho gradient bằng khóa \(i\). Sự giống nhau giữa phương pháp này và việc lưu trữ (khóa, giá trị) như phương pháp được giới thiệu trong Dynamo [DeCandia et al., 2007] không phải là ngẫu nhiên. Chúng thỏa mãn rất nhiều tính chất, cụ thể là khi phân phối các tham số cho nhiều máy chủ.

  • đẩy (khóa, giá trị) gửi một gradient cụ thể (giá trị) từ máy thợ đến thiết bị lưu trữ chung. Tại đây các tham số được tổng hợp lại, ví dụ bằng cách lấy tổng.
  • kéo (khóa, giá trị) lấy lại tham số đã được tổng hợp từ thiết bị lưu trữ chung, sau khi đã kết hợp gradient từ tất cả máy thợ.

Bằng cách ẩn đi sự phức tạp của việc đồng bộ sau các thao tác đơn giản là đẩy và kéo, ta có thể tách những mối bận tâm theo hai hướng: của các nhà mô hình thống kê, những người muốn biểu diễn việc tối ưu một cách đơn giản và các kỹ sư hệ thống, những người cần giải quyết sự phức tạp sẵn có trong việc đồng bộ hóa phân tán. Trong phần tiếp theo ta sẽ thử nghiệm việc lưu trữ (khóa, giá trị) trong thực tế.

12.7.5. Tóm tắt

  • Việc đồng bộ cần có độ thích ứng cao với hạ tầng mạng cụ thể và kết nối trong máy chủ. Điều này có thể tạo ra khác biệt đáng kể trong thời gian đồng bộ.
  • Đồng bộ dạng vòng có thể là phương án tối ưu với các máy chủ P3 và DGX-2, còn với các loại máy chủ khác thì không hẳn.
  • Chiến lược đồng bộ phân cấp rất tốt khi thêm nhiều máy chủ tham số để tăng băng thông.
  • Giao tiếp bất đồng bộ (khi việc tính toán vẫn đang diễn ra) có thể cải thiện hiệu năng.

12.7.6. Bài tập

  1. Bạn có thể cải thiện đồng bộ dạng vòng hơn nữa không? Gợi ý: bạn có thể gửi thông tin theo cả hai chiều.
  2. Đồng bộ bất đối xứng hoàn toàn có độ trễ nào không?
  3. Nên để khả năng chịu lỗi (fault tolerance) như thế nào? Nếu một máy chủ gặp trục trặc thì sao? Đây có phải vấn đề nghiêm trọng không?
  4. Lưu checkpoint như thế nào?
  5. Bạn có thể tăng tốc việc tổng hợp dạng cây (tree aggregation) không?
  6. Tìm hiểu các cách rút gọn khác (như dạng bán vòng giao hoán - commutative semiring).

12.7.7. Thảo luận

12.7.8. 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 Mai Hoàng Long
  • Phạm Hồng Vinh
  • Lê Khắc Hồng Phúc
  • Nguyễn Văn Cường
  • Đỗ Trường Giang
  • Nguyễn Thanh Hòa
  • Phạm Minh Đức
  • Nguyễn Lê Quang Nhật