.. raw:: html
.. _sec_parameterserver:
Máy chủ Tham số
===============
.. raw:: html
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.
.. raw:: html
Ý tưởng cốt lõi của máy chủ tham số được đề xuất từ
:cite:`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 :cite:`Ahmed.Aly.Gonzalez.ea.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 :cite:`Li.Andersen.Park.ea.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.
.. raw:: html
Huấn luyện Song song Dữ liệu
----------------------------
.. raw:: html
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. :numref:`fig_parameterserver` 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.
.. raw:: html
.. _fig_parameterserver:
.. figure:: ../img/ps.svg
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.
.. raw:: html
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ố :math:`\mathbf{v}_1, \ldots, \mathbf{v}_4` với các gradient tương ứng
là :math:`\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.
.. math:: \mathbf{g}_{i} = \sum_{j \in \mathrm{GPU}} \mathbf{g}_{ij}
.. raw:: html
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 ở :numref:`sec_hardware`. Xét một máy chủ
GPU 4-chiều được mô tả trong :numref:`fig_bw_hierarchy`. 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.
.. raw:: html
.. _fig_bw_hierarchy:
.. figure:: ../img/bw-hierarchy.svg
Một máy chủ GPU 4-chiều.
.. raw:: html
Để 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.
:numref:`fig_ps_distributed` minh họa sự khác biệt giữa các chiến lược
trao đổi tham số khác nhau.
.. raw:: html
.. _fig_ps_distributed:
.. figure:: ../img/ps-distributed.svg
Các chiến lược đồng bộ
.. raw:: html
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 :cite:`Sergeev.Del-Balso.2018` để biết chi
tiết cách làm điều này trong
`Horovod `__.
.. raw:: html
Đồng bộ dạng Vòng
-----------------
.. raw:: html
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
:numref:`fig_nvlink`. 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 đó.
.. raw:: html
.. _fig_nvlink:
.. figure:: ../img/nvlink.svg
Kết nối NVLink trên các máy chủ 8 GPU V100 (hình ảnh được sự đồng ý
từ NVIDIA).
.. raw:: html
Hóa ra theo :cite:`Wang.Li.Liberty.ea.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. :numref:`fig_nvlink_twoloop` 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.
.. raw:: html
.. _fig_nvlink_twoloop:
.. figure:: ../img/nvlink-twoloop.svg
Phân tách mạng NVLink thành hai kết nối dạng vòng.
.. raw:: html
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ó
:math:`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 :math:`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 :math:`n` khúc và bắt đầu đồng bộ khúc
thứ :math:`i` tại thiết bị :math:`i`? Vì mỗi khúc có kích thước
:math:`1/n`, tổng thời gian giờ sẽ là :math:`(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.
:numref:`fig_ringsync` minh họa chuỗi các bước với số thiết bị
:math:`n=4`.
.. raw:: html
.. _fig_ringsync:
.. figure:: ../img/ringsync.svg
Đồ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ó.
.. raw:: html
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ỉ
:math:`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.
.. raw:: html
Huấn luyện trên Nhiều Máy tính
------------------------------
.. raw:: html
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ộ. :numref:`fig_ps_multimachine` mô
tả quá trình huấn luyện phân tán song song.
.. raw:: html
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.
.. raw:: html
.. _fig_ps_multimachine:
.. figure:: ../img/ps-multimachine.svg
Huấn luyện song song phân tán trên nhiều máy tính đa GPU
.. raw:: html
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 :math:`m` máy thợ, thời gian để truyền toàn bộ
gradient đến máy chủ là :math:`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 :math:`n`. Khi đó mỗi máy chủ chỉ
cần lưu trữ :math:`O(1/n)` tham số, do đó tổng thời gian cần để cập nhật
và tối ưu trở thành :math:`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ợ. :numref:`fig_ps_multips`
minh hoạ thiết kế này. Độc giả có thể tham khảo
:cite:`Li.Andersen.Park.ea.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.
.. raw:: html
.. _fig_ps_multips:
.. figure:: ../img/ps-multips.svg
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.
.. raw:: html
Lưu trữ (Khóa, Giá trị)
-----------------------
.. raw:: html
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
.. math:: \mathbf{g}_{i} = \sum_{k \in \mathrm{máy~thợ}} \sum_{j \in \mathrm{GPU}} \mathbf{g}_{ijk}.
.. raw:: html
Đặ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 :math:`i`
gắn liền với các tham số (và các gradient) khác nhau.
.. raw:: html
Đ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
:math:`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
:cite:`DeCandia.Hastorun.Jampani.ea.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ủ.
.. raw:: html
- **đẩ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ợ.
.. raw:: html
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ế.
.. raw:: html
Tóm tắt
-------
.. raw:: html
- 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.
.. raw:: html
Bài tập
-------
.. raw:: html
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*).
Thảo luận
---------
- `Tiếng Anh `__
- `Tiếng Việt `__
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