Thứ Ba, tháng 1 24, 2017

Phan Hầm Mỏ - End of the road

Tết về, phố đông chật người rồi chợt bỗng vắng lặng đường khuya. Một mình bước đi trên con phố nhỏ dưới cơn mưa bụi, lặng lẽ ra ga tàu. Vừa đi, vừa miên man suy ngẫm về câu chuyện "Sống là không chờ đợi". Liệu còn gì mà tôi chưa làm hết mình, còn gì tôi chưa sống hết mình mỗi ngày qua, có gì mà tôi "đánh mất" và "nhặt được" trên chặng đường mình đi hay không? Câu chuyện ấy viết rằng "Đừng bao giờ giữ lại một cái gì để chờ dịp đặc biệt cả... Mỗi ngày sống là một dịp đặc biệt rồi..."

Cuộc đời con người, có nhiều quyết định đúng, có nhiều quyết định sai. Rồi có những cái lấp lửng giữa cả 2 nữa, mà không thể minh bạch được đúng hay sai. Ngoài ra, cũng còn theo chiều thời gian nữa. Tôi nhớ lại những cái vốn rất sai của mình bây giờ lại có điểm quá đúng.

Chợt thấy mình lẻ loi. Mưa dần rơi nặng hạt cũng chẳng buồn dương ô lên nữa. Con đường nhỏ len lỏi trong khu dân cư, không một bóng người, trời khuya không một ánh đèn cửa sổ, chỉ còn ánh sáng yếu ớt từ vài ngọn đèn đường lưa thưa và vừng sáng từ các máy bán nước uống tự động. Đôi giày sũng nước mưa từ chiều nặng bên tay xách, tôi bỏ nốt đôi dép mà mấy em đồng nghiệp đưa cho, chân không rảo bước. Nước dưới chân mát lạnh, không hề gây khó chịu mà chỉ giúp nhanh bước hơn ra sân ga.

Thông thường, ở một thành phố xa lạ, nếu cuối tuần trời mưa thì cũng có nghĩa là ông trời muốn giam bước chân người xa nhà, chỉ vào mạng, ngủ vùi đầu, xem TV hay viết blog thôi chẳng hạn... Sau rất nhiều lần thành công, lần này, ông ấy đã thất bại vì mình "sổng" từ tối thứ 6 đến Chủ Nhật mới về. Và vì thế, mưa cứ luấn quấn bước chân, dò hỏi xem "anh buồn à?", "anh muốn gì à?" 

Ừ. Thì tôi buồn, nỗi buồn của một kẻ hành khất cô đơn, kiêu ngạo cố tỏ vẻ không còn cần gì nữa, không muốn gì nữa.

Ừ. Thì tôi lại muốn rồi. Mâu thuẫn chưa? Tôi muốn có ai đó cùng đi trong mưa. Cùng sánh bước trong ướt lạnh, chân không giầy, dẫm nước, miễn là bàn tay còn ấm. Tự hỏi mình, khi như thế, có còn cần đi tiếp ra sân ga nữa không đây? Bất chợt nhận ra, trong muôn vàn hạt mưa bụi bay bay, táp vào mặt, có giọt âm ấm đọng lại.

Văng vẳng trong đầu, câu hát vu vơ, lúc mờ lúc rõ của một người bạn thân ôm đàn guitar, ngân nga đêm nào. Có những lúc, nỗi buồn khi cô đơn làm người ta vui.

Ta tưởng yêu thương là sẽ cùng nhau đi tận cuối con đường. Hóa ra không hẳn vậy, chỉ là tới cuối con đường ta mới kịp nhận ra điều đó thôi. Mà yêu thương cũng không dừng lại khi đến cuối đường đâu, như cuối con đường này sẽ là 1 con đường mới. Cuối năm này sẽ có một năm mới với bao dự định ấp ủ mới.

Gửi 3S Radio Tết con gà 2017 một bài hát rất xưa cũ nhé. End of the road của Boys2Men với lời nhắn, Chúc các bạn nghe đài một năm mới Tết đoàn viên và yêu thương, hãy để tim rộng mở và cảm nhận yêu thương trên từng giây phút và từng bước chân đi, đừng chờ đợi đến một nẻo cuối con đường đâu đó nữa ấy.

Thứ Sáu, tháng 1 20, 2017

Kidnapping - Bắt cóc?

3 năm trước, tôi là cái áo khoác cậu bé rúc vào khi lạnh, là cái ghế cậu bé ngồi lên, là doctor Know-It-All để cậu bé mang mọi câu hỏi đến.
2 năm trước, một kẻ ích kỷ đã cho mình cái quyền tước đi mối thâm tình đó.
Cậu bé dần dà bị dẫn dắt bởi "... con không đi chơi với bố, không về nhà mình sợ bố bắt cóc con..."
Quặn thắt! Nước mắt rơi trong tim!
Không thể cất nổi lời. Chỉ tâm niệm Time will tell the truth!
Hôm nay, cậu vẫn không đi với bố nhưng đã biết tự khẳng định được: "...bố chẳng bắt cóc con đâu mà..."
Lái xe đêm muộn, chợt bất giác ngân nga hát theo radio FM:
"...  Bỗng thấy yêu đời quá, yêu ngày xanh nắng vàng
Bỗng thấy yêu thời gian, hạnh phúc đến nhẹ nhàng.
Yêu sao những tiếng cười và những khi bên người.
Ngày chẳng còn lo toan mệt nhoài.
Cuộc đời vì thế nên đẹp tuyệt vời!
... Có lúc tôi gục ngã, nhìn ngày trôi hững hờ.
Có lúc tôi thầm mơ sẽ hái sao trên trời, 
Mà nào có biết rằng, hạnh phúc luôn bên mình là những điều nhỏ nhoi thường ngày mà tôi tìm mãi nơi phù du."

Một điều nhỏ nhoi ấy, cậu bé đã trao tôi, trưa nay.


Thứ Năm, tháng 1 19, 2017

ĐÔI NÉT VỚI TẬP LỆNH NCCL ĐA GPU (GRAPHIC PROCESSING UNIT: VI XỬ LÝ ĐỒ HỌA)


Ngày nay, nhiều máy chủ chứa 8 GPU hoặc nhiều hơn. Trên nguyên tắc đó, chạy một ứng dụng sử dụng một đến nhiều GPU đúng ra sẽ mang lại một hiệu suất gấp bội. Nhưng thực tế thì khá khó để tận dụng được lợi thế này. Có hai nguyên nhân cơ bản phía sau sự thiếu tính khả mở của hệ thống đa-GPU. Đầu tiên ứng không có đủ luồng xử lý song song để dùng hết các vi xử lý trên GPU. Lý do thứ hai gây khó khăn khi tận dụng mở rộng ở chỗ các bộ vi xử lý sẽ phải dành thời gian để trao đổi dữ liệu với nhau quá nhiều thay vì tập trung chạy thuật toán. Để giảm tình trạng nút cổ chai ấy, điều chính yếu cần thiết phải tạo được băng thông lớn nhất sẵn sàng cho các kết nối liên GPU. NCCL là một đáp án.

NCCL (phát âm là "Nickel") là một thư viện tập lệnh gốc đa-GPU có hướng cấu trúc liên kết và có thể dễ dàng tích hợp vào các ứng dụng. Khởi thủy từ một dự án nghiên cứu mã nguồn mở, NCCL được thiết kế để có dung lượng thật nhỏ dựa trên C ++ thông thường và các thư viện CUDANCCL có thể được ứng dụng vào trong các phần mềm đơn luồng hoặc đa luồng, để quản lý truyền nhận tin giữa các luồng xử lý hiệu quả. Sau nữa, NCCL đã bao gồm sẵn các API vốn rất quen thuộc với bất cứ ai có kinh nghiệm sử dụng các tập lệnh MPI.

Hình 1: Minh họa về tập lệnh All-Reduce.

Tập lệnh giao thức

Các chương trình giao thức tổ hợp là mẫu hình chung của truyền dữ liệu giữa nhiều bộ vi xử lý. Nếu đã có kinh nghiệm với MPI thì có lẽ cũng đã quen thuộc với một số phép toán tổ hợp lệnh. Ví dụ, All-Reduce (Giảm toàn phần) bắt đầu với mảng độc lập $latex V_k$ của N giá trị trên K bộ vi xử lý và kết thúc với mảng giống hệt S của N giá trị, trong đó $S[k] = V_0[k] + V_1[k] + ... + V_k[k]$, cho mỗi bộ xử lý k . Xem hình 1.
Một giao thức tổ hợp thông thường khác là All-Gather (Nhóm toàn phần), trong đó mỗi bộ xử lý trong tập hợp K bắt đầu với một mảng độc lập của N giá trị và thu thập dữ liệu từ tất cả các bộ xử lý khác để tạo thành một kết quả mảng N*K , như minh họa ở hình 2.

Hình 2: Minh họa về All-gather.
Broadcast (Tán xạ) là một ví dụ thứ ba. Dưới đây là một thành phần đệm gồm N phẩn tử trên một bộ xử lý được sao chép vào tất cả các bộ xử lý khác, như hình 3 cho thấy.
Hình 3: Minh họa về Broadcast
Tất cả các tổ hợp trên có thể được thực hiện ở vùng "ngoại vi", có bộ đệm vào và ra riêng biệt, hoặc "nội hàm" có chồng lấn vào và ra.
Hình 4: Giao thức PCIe chung cho 4 GPU gắn trong hệ thống có một CPU duy nhất. Mũi tên màu đỏ đại diện cho các kết nối băng thông PCIe x16.
Có rất nhiều phương pháp để thực hiện tổ hợp hiệu quả. Tuy nhiên, điểm then chốt khi thực hiện thì phải cân nhắc đến kiến trúc giao thức của liên kết giữa các bộ vi xử lý. Ví dụ như ở giao thức nhánh cây PCIe trong hình dưới đây về đường tán xạ dữ liệu từ GPU0 đến tất cả các GPU khác.
Thuật toán nhánh cây hai bước là một lựa chọn phổ biến ở đây. Tại bước đầu tiên, dữ liệu được gửi từ GPU0 tới một GPU khác, và trong bước thứ hai cả hai sẽ gửi dữ liệu đến các bộ vi xử lý còn lại. Dù thế thì vẫn có nhiều cách thực thi khác nhau. Dữ liệu có thể được gửi từ GPU0 đến GPU1 trong bước đầu tiên và sau đó từ GPU0 đến GPU2 và từ GPU1 đến GPU3 ở bước hai, hoặc có thể thực hiện dưới hình thức bản sao ban đầu từ GPU0 đến GPU2, kế đó ở bước hai thì từ GPU0 đến GPU1 và GPU2 đến GPU3. Kiểm tra cấu trúc liên kết, cách thứ hai tốt hơn rõ rệt, vì nếu gửi dữ liệu đồng thời từ GPU0 đến GPU2 và GPU1 đến GPU3 sẽ gây tranh chấp tài nguyên trên các liên kết PCIe, hiệu suất băng thông ở bước này bị giảm một nửa. Nói chung, để đạt được hiệu quả cao đối với tổ hợp lệnh đòi hỏi sự quan tâm cẩn trọng đến cấu trúc liên kết.
Để tối ưu hóa băng thông Broadcast, còn một cách tiếp cận tốt hơn là tạo liên kết cấu trúc vòng PCIe.
Tán xạ được thực hiện bằng việc sắp xếp các tập nhỏ dữ liệu đầu vào xung quanh vòng từ GPU0 để GPU3. Điều thú vị là, các thuật toán vòng cung cấp băng thông cận tối ưu cho hầu hết tất cả các thực thi tổ hợp lệnh tiêu chuẩn, thậm chí khi áp dụng cho cấu trúc PCIe kiểu "nhánh cây". Tất nhiên phải lưu ý rằng việc lựa chọn đúng chiều tuần hoàn của vòng vẫn rất quan trọng.

Hình 5: Vòng tuần hoàn của GPU trong cây PCIe.

Tập lệnh GPU với NCCL

Để cung cấp băng thông tối đa, NCCL áp dụng các tập lệnh tổ hợp kiểu vòng. NCCL ngầm đánh chỉ số GPU theo trật tự vòng tối ưu. Điều này cung cấp hiệu suất tuyệt vời cho ứng dụng mà nhà phát triển không còn phải lo lắng về cấu hình phần cứng cụ thể ra sao.
Nhiều tổ hợp lệnh đòi hỏi một bộ đệm chứa kết quả trung gian. Để giảm thiểu dung lượng chiếm bộ nhớ xuống còn một vài MB trên mỗi GPU, NCCL chia tách tổ hợp lệnh lớn thành nhiều phần nhỏ. Hiệu quả cực thấp nếu khởi tạo lõi xử lý và thực thi gọi hàm cudaMemcpy riêng biệt cho mỗi công đoạn hay từng phần nhỏ của cùng một thuật toán tổ hợp. Thay vào đó, mỗi tổ hợp được thực hiện trọn vẹn trên một nhân CUDANCCL tận dụng tối đa truy cập trực tiếp của giao thức GPUDirect Peer-to-Peer để truyền dữ liệu giữa các bộ xử lý. Trường hợp không lấy được truy cập trực tiếp peer-to-peer (ví dụ, khi phải truyền qua một kết nối QPI), thì các dữ liệu đã truyền đi được tổ chức thông qua một bộ đệm trong một vùng xác định trên bộ nhớ hệ thống. Tương tự như vậy, đồng bộ dũ liệu được thực hiện bằng cách kiểm tra các biến số biến đổi trong vùng nhớ của card (hoặc vùng nhớ xác định của hệ thống).
Bên trong, NCCL quy việc thực hiện mỗi tổ hợp lệnh về 3 nguyên hàm: Copy, Reduce, và ReduceAndCopy. Mỗi nguyên hàm được tối ưu hóa để truyền đi một lượng dữ liệu tinh giản (4-16KB) giữa các GPU. Các lõi xử lý cũng đã được tối ưu hóa đặc biệt để đạt được băng thông tối đa trong khi lại chiếm dụng thấp. Kết quả là, NCCL có thể sử dụng tối đa tải một kết nối PCIe 3.0 x16 mà chỉ bằng một đơn vị tổ hợp luồng xử lý CUDA. Điều này khiến giải phóng phần lớn GPU dùng vào chạy các tác vụ tính toán đồng thời với truyền tải thông tin.

NCCL hiện hỗ trợ các tập lệnh all-gather, all-reduce, broadcast, reduce, và reduce-scatter, không hạn chế số lượng GPU được sử dụng, miễn là nằm trên cùng một máy.

Sử dụng NCCL

Mã nguồn mở NCCL có sẵn trên Github . Các thư viện có thể chạy trên bất kỳ phiên bản Linux phổ biến và cũng tương thích với CUDA 7.0 hay mới hơn. Hiện giờ, CUDA chỉ hỗ trợ truy cập trực tiếp giữa các GPU cùng kiến trúc và phải nằm trên cùng một máy (PCIe root hub). GPU không phù hợp các tiêu chí này vẫn được hỗ trợ bởi NCCL, mặc dù hiệu suất sẽ được giảm do việc chuyển dữ liệu sẽ đi vòng thông qua vùng nhớ xác định trong bộ nhớ hệ thống.
Các NCCL API đều tuân thủ chặt theo MPI. Trước khi thực thi các tập lệnh, một đối tượng truyền tin phải được tạo ra trên mỗi GPU. Các đối tượng này xác định các thiết lập của các GPU tham gia vào tổ hợp và lập bản đồ các luồng thông tin liên lạc giữa chúng. Tập hợp đối tượng giao tiếp liên quan với nhau được gọi là một clique (phe). Có hai cách để khởi tạo đối tượng truyền tin trong NCCL. Phương pháp phổ thông nhất là gọi hàm ncclCommInitRank() tương ứng với mỗi GPU.

ncclResult_t ncclCommInitRank(ncclComm_t* comm,
int nGPUs, ncclUniqueId cliqueId,
int rank);
Chức năng này giả định rằng các GPU thuộc các hàng xác định đã được thiết lập thông qua cudaSetDevice()nGPUs là số lượng GPU trong một clique. cliqueId cho phép các hàng trong cùng clique nhận diện nhau. Các hàng thì sẽ có cùng một cliqueId. Để làm vậy, hàm ncclGetUniqueId() được gọi từ trong một hàng và sẽ phát tán kết quả trả về uniqueId đến các hàng khác còn lại trong clique thông qua mô hình truyền thông tùy chọn (ví dụ, MPI_Bcast).
Các biến số cuối cùng trong ncclCommInitRank() xác định các chỉ số của GPU hiện tại trong clique. Nó phải là duy nhất cho mỗi cấp trong clique và trong tập hợp [0, nGPUs]. Chỉ số này được sử dụng, ví dụ, để xác định các GPU nguồn cho lệnh broadcast, hoặc để truy xuất đến theo lệnh all-gather.
Sau khi khởi tạo thành công, ncclCommInitRank() trả về ncclSuccess và *comm được thiết lập chứa đối tượng truyền tin mới của NCCL.
Bên trong, ncclInitRank() thực hiện đồng bộ giữa tất cả các đối tượng truyền tin trong cùng một clique. Như vậy, nó phải được gọi từ các luồng khởi tạo khác nhau cho từng GPU, hoặc từ các xử lý riêng biệt (ví dụ, MPI ranks). Cụ thể, nếu gọi ncclInitRank() trong cùng một vòng lặp xử lý cho các GPUs sẽ dẫn đến tình trạng xung đột khóa chết (deadlocks)
Cách thứ hai để khởi tạo các đối tượng truyền tin là sử dụng ncclCommInitAll(). Đây thực chất là một thói quen tiện dụng để tiết kiệm việc phải sinh thêm các luồng khởi tạo mới để chạy NCCL trong trình ứng dụng đơn nhiệm.

ncclResult_t ncclCommInitAll ( ncclComm_t * comms ,
int nGPUs ,
int * devList );
Biến comms đang trỏ đến một mảng các đối tượng ncclComm_t, tương ứng cho mỗi hàng chứa nGPUs trong cùng 1 clique. devList thì quy định cụ thể thiết bị CUDA nào tương ứng với từng hàng. Khi khởi tạo các đối tượng truyền tin, ta có thể gọi các tập lệnh thông qua các hàm gốc của chúng, chẳng hạn như ncclAllReduce().
ncclResult_t ncclAllReduce ( void* sendoff ,
void* recvbuff , int count ,
ncclComm_t comm ,
ncclDataType_t type , ncclRedOp_t op ,
cudaStream_t stream);
Tài liệu chi tiết cho mỗi tập lệnh được cung cấp trong file nccl.h. Hầu hết các biến đều tương tự như trong tập lệnh API của MPI. Trường hợp ngoại lệ nổi bật là các biến dòng (stream). Tương tự như rất nhiều chương trình CUDA "Async", lệnh NCCL sẽ lên lịch thực thi trong một dòng, nhưng có thể trả lại trước khi tập lệnh được hoàn tất. Bằng cách xếp các tập lệnh vào hàng đợi còn nhân tính toán riêng khác ở các dòng độc lập, GPU có thể chồng lấn nhiều hơn công việc đòi hỏi tính toán tập trung cao vào cùng thời điểm truyền tập lệnh. Nhằm tối đa hóa sự chồng lấn này, các tập lệnh NCCL được lập lịch chạy trên dòng có độ ưu tiên cao để cho phép chúng trượt qua lại trong các lưới tính toán tập trung.
Ứng với mỗi đối tượng truyền tin trong một clique đều phải gọi đến ncclAllReduce(), mỗi lần gọi được thực hiện qua bộ đệm gửi và nhận riêng biệt, v.v. Tập lệnh NCCL sẽ giả định ngữ cảnh CUDA tương ứng là mới nhất. Và bởi vì NCCL chạy không đồng bộ, một vòng lặp đơn giản có thể được sử dụng để khởi tạo một tập lệnh từ một ứng dụng đơn luồng.

for ( int gpu = 0 ; gpu < nGPUs ; ++ gpu ) {
cudaSetDevice ( devList [ gpu ]);
}
ncclAllReduce (...);

Hiệu suất

Hình 6: Liên kết băng thông đạt được của tập thể NCCL khác nhau.
Hiệu suất mà tập lệnh NCCL đạt được phụ thuộc vào chính cấu trúc liên kết của máy tính. Trong trường hợp tốt nhất, tất cả các GPU đều chiếm phần truy cập như nhau. Đây là mô hình phổ biến nhất ở các máy trạm được trang bị vài GPU. Các máy chủ lớn hơn thường gắn với CPU kép, và một số lượng các GPU tách biệt trên các hub IO khác nhau. Hình 6 cho thấy băng thông NCCL tương ứng đối với các tập hợp lệnh khác nhau đo trên máy NVIDIA Digits DevBox trang bị 4 GPU GeForce GTX Titan X được ráp theo mô hình ở hình 4 trên đây.
Đường màu đỏ 10.4 GB/ s đại diện cho băng thông sử dụng bở một lệnh cudaMemcpy dự liệu lớn giữa hai trong số các GPU (cụ thể là GPU 0 và 2 trong hình 4). NCCL vẫn có thể duy trì tỷ lệ cao khả năng chiếm dụng băng thông cao điểm đó khi thực hiện giao tiếp giữa tất cả bốn GPU.

Hướng phát triển trong tương lai và một số điểm lưu tâm

Mục tiêu của NCCL là để cung cấp tập lệnh tổ hợp tự nhận thức được cấu trúc liên kết nhằm có thể cải thiện độ khả mở của các ứng dụng đa GPU. Sử dụng NCCL sẽ có thể giúp đạt được hiệu suất tuyệt vời mà không phải chú ý về các chi tiết phần cứng bậc thấp.
Nhữn mẩu mã nguồn ví dụ đơn giản cho cả hai loại ứng dụng trình đơn và đa luồng MPI cũng được kèm sẵn trong bộ thư viện NCCL. Xuất phát từ một dự án nghiên cứu, mọi phản hồi cuả các nhà phát triển đểu được hoanh nghênh vì dự án này vẫn còn tiếp tục. Hãy dùng thử và chia sẻ bạn nghĩ gì nhé! Để cập nhật về những thông tin mới nhất, hãy đến dự buổi nói chuyện NCCL của tôi ở GTC.