Tôi đang sử dụng C++ và CUDA/C và muốn viết mã cho một vấn đề cụ thể, nhưng tôi gặp phải một vấn đề đơn giản hóa rất khó khăn.
Kinh nghiệm của tôi với lập trình song song không phải là không đáng kể nhưng khá hạn chế, và tôi không thể lường trước được hết những đặc thù của vấn đề này.
Tôi nghi ngờ có cách thuận tiện hay thậm chí là "dễ dàng" nào để giải quyết vấn đề tôi đang gặp phải, nhưng có lẽ tôi đã nhầm.
Nếu có bất kỳ tài nguyên nào (ví dụ: bài viết, sách, liên kết web, v.v.) hoặc từ khóa nào giải quyết vấn đề này hoặc các câu hỏi tương tự, vui lòng cho tôi biết.
Tôi đã cố gắng khái quát toàn bộ trường hợp này càng nhiều càng tốt và giữ cho nó trừu tượng thay vì đăng quá nhiều mã.
cách trình bày...
Tôi có một hệ thống gồm N phần tử ban đầu và N phần tử kết quả. (Ví dụ, tôi sẽ sử dụng N = 8, nhưng N có thể là bất kỳ số nguyên nào lớn hơn 3.)
kích thước tĩnh_t const N = 8;
giá trị khởi tạo kép[N], kết quả[N];
Tôi cần tính toán hầu hết mọi (nếu không muốn nói là tất cả) hoán vị duy nhất của giá trị init mà không làm ảnh hưởng đến chính mình.
Điều này có nghĩa là phép tính
f(giá_trị_khởi_đầu[0],giá_trị_khởi_đầu[1])
,
f(giá_trị_khởi_đầu[0],giá_trị_khởi_đầu[2])
, ...,
f(giá_trị_khởi_đầu[0],giá_trị_khởi_đầu[N-1])
,
f(giá_trị_khởi_đầu[1],giá_trị_khởi_đầu[2])
, ...,
f(giá_trị_khởi_đầu[1],giá_trị_khởi_đầu[N-1])
,...vân vân.
Trên thực tế, đây là một ma trận tam giác ảo và hình dạng của nó được thể hiện ở hình bên dưới.
P0 1 2 3 4 5 6 7
|---------------------------------------
0|x
|
1| 0x
|
2| 1 2 lần
|
3| 3 4 5 lần
|
4| 6 7 8 9 lần
|
5| 10 11 12 13 14 lần
|
6| 15 16 17 18 19 20 lần
|
7| 21 22 23 24 25 26 27 lần
Mỗi phần tử là
giá trị khởi tạo
Chức năng của các phần tử cột và hàng tương ứng trong .
P[i] (= P[hàng(i)][cột(i]) = f(giá_trị_ban_đầu[cột(i)], giá_trị_ban_đầu[hàng(i)])
Ngay lập tức
P[11] (= P[5][1]) = f(giá_trị_ban_đầu[1], giá_trị_ban_đầu[5])
Ví dụ sử dụng
(N*NN)/2 = 28
,có
P[1][5]==P[5][1]
Có thể là sự kết hợp duy nhất (Lưu ý:
Số lượng = 8
, do đó chúng ta chỉ có ma trận tam giác dưới (hoặc trên).
Câu hỏi cơ bản
Tính toán mảng kết quả từ P bằng tổng các phần tử hàng trừ đi tổng các phần tử cột riêng lẻ.
Ví dụ, kết quả ở vị trí 3 sẽ được tính bằng tổng của hàng 3 trừ đi tổng của cột 3.
kết quả[3] = (P[3]+P[4]+P[5]) - (P[9]+P[13]+P[18]+P[24])
kết quả[3] = tổng_phần_tử_hàng(3) - tổng_phần_tử_cột(3)
Tôi đã cố gắng minh họa trong hình cho N=4.
Do đó, phát biểu sau đây là đúng:
N-1
Các hoạt động (có khả năng ghi đồng thời) sẽ được thực hiện trên mỗikết quả[i]
Thực hiện trên
kết quả[i]
Bằng cách trừ vàN-(i+1)
Phép cộng choTôi
Viết
Từ mỗiP[i][j]
Nội dung được truyền tải sẽ làr[j]
Thực hiện phép trừ vàr[tôi]
Thực hiện phép cộng
Đây chính là nơi mà vấn đề chính nảy sinh:
Việc sử dụng một luồng để tính toán từng P và cập nhật kết quả trực tiếp sẽ dẫn đến việc nhiều lõi cố gắng ghi vào cùng một vị trí kết quả (một lõi cho mỗi N-1 luồng).
Mặt khác, việc lưu trữ toàn bộ ma trận P cho các bước giảm tiếp theo rất tốn kém về mặt bộ nhớ và do đó không khả thi đối với các hệ thống lớn.
Ý tưởng có một vectơ kết quả duy nhất, được chia sẻ cho mỗi khối luồng cũng không khả thi.
(N trong 50k tạo ra 2,5 tỷ phần tử P, vì vậy [giả sử tối đa 1024 luồng cho mỗi khối] nếu mỗi khối có mảng kết quả riêng (với 50k phần tử double), khối nhỏ nhất là 2,4 triệu sẽ tiêu tốn 900GiB bộ nhớ.)
Tôi nghĩ tôi có thể xử lý việc giảm bớt để có được hành vi tĩnh hơn, nhưng vấn đề này khá năng động xét về khả năng truy cập ghi bộ nhớ đồng thời.
(Hoặc có thể xử lý vấn đề này bằng một số biện pháp giảm thiểu "cơ bản" nào đó không?)
Để thêm phần phức tạp...
Thật không may, tùy thuộc vào dữ liệu đầu vào (tùy ý của người dùng), không phụ thuộc vào các giá trị ban đầu, một số phần tử của P cần phải bị bỏ qua.
Giả sử chúng ta cần bỏ qua các hoán vị P[6], P[14] và P[18]. Do đó, chúng ta vẫn còn 24 tổ hợp để tính toán.
Làm thế nào để tôi có thể cho kernel biết giá trị nào cần bỏ qua?
Tôi đã đưa ra ba cách tiếp cận, mỗi cách đều có những nhược điểm đáng kể nếu N rất lớn (chẳng hạn như hàng chục nghìn phần tử).
1. Lưu trữ tất cả các kết hợp...
...và các chỉ số hàng và cột tương ứng của chúng
struct combo { size_t hàng, cột; };
, cần phải
vector
Tính toán và vận hành trên vectơ này. (Được sử dụng bởi bản triển khai hiện tại)
phần tử std::vector;
// bằng cách nào đó điền vào
size_t const M = phần tử.size();
đối với (size_t i=0; i
{
// thực hiện các tính toán cần thiết bằng cách sử dụng elements[i].row và elements[i].col
}
Vì chỉ có "một vài" (thậm chí có thể là 10 nghìn phần tử, nhưng điều đó không quan trọng lắm so với hàng tỷ phần tử), giải pháp này tiêu tốn rất nhiều bộ nhớ, nhưng nó tránh được
Tính toán chỉ số
Tìm phần tử đã xóa
đối với mỗi phần tử của P, đây là nhược điểm của cách tiếp cận thứ hai.
2. Thao tác trên tất cả các phần tử của P và tìm các phần tử đã xóa
Nếu tôi muốn vận hành trên từng phần tử của P và tránh các vòng lặp lồng nhau (không thể tái tạo tốt trong cuda), tôi cần thực hiện như sau:
kích thước_t M = (N*NN)/2;
đối với (size_t i=0; i
{
// tính toán chỉ số hàng từ `i`
tmp kép = sqrt(8.0*double(i+1))/2.0 + 0.5;
hàng đôi_d = sàn(tmp);
size_t hàng hiện tại = size_t(hàng_d);
size_t cột_hiện_tại = size_t(sàn(hàng_d*(ict-hàng_d)-0,5));
// kiểm tra xem tổ hợp hàng và cột hiện tại có bị xóa không
nếu (!removes[current_row].exists(current_col))
{
// thực hiện các tính toán cần thiết bằng cách sử dụng current_row và current_col
}
}
So sánh với ví dụ đầu tiên
loại bỏ
So với vector
các yếu tố
Rất nhỏ, nhưng hữu ích để có được
hàng hiện tại
,
cột hiện tại
Việc tính toán thêm nhánh if rất kém hiệu quả.
(Hãy nhớ rằng chúng ta vẫn đang nói về hàng tỷ lượt đánh giá.)
3. Vận hành tất cả các phần tử của P và sau đó xóa các phần tử
Một ý tưởng khác của tôi là đếm tất cả các kết hợp hợp lệ và không hợp lệ một cách độc lập.
Nhưng thật không may, câu lệnh sau đây là đúng do tổng sai:
calc_non_skipped() != calc_all() - calc_skipped()
Có cách nào thuận tiện, hiệu suất cao và được biết đến để có được kết quả mong muốn từ các giá trị ban đầu không?
Tôi biết câu hỏi này khá phức tạp và có thể không mấy liên quan. Tuy nhiên, tôi hy vọng một số câu trả lời sáng tỏ sẽ giúp tôi hiểu ra vấn đề.
Thực hiện hiện tại
Hiện tại, điều này được thực hiện dưới dạng mã CPU thông qua OpenMP.
Đầu tiên tôi xây dựng một
kết hợp
Một vectơ lưu trữ từng giá trị P cần tính toán và truyền nó vào vòng lặp for song song.
Mỗi luồng có một vectơ kết quả riêng và phần quan trọng ở cuối vùng song song được sử dụng để tính tổng một cách thích hợp.
Trước hết, tôi hơi bối rối. Tại sao(N**2 - N)/2
Với N=7 sẽ tạo ra 27... nhưng đối với các chỉ số từ 0-7, với N=8, sẽ có 28 phần tử trong P. Vì vậy, đừng cố gắng trả lời những câu hỏi như thế này ngay tối nay hoặc ngày hôm đó. :-)
Nhưng có một giải pháp tiềm năng: Bạn có cần giữ mảng P cho mục đích nào khác không? Nếu không, tôi nghĩ bạn có thể nhận được kết quả mong muốn bằng cách sử dụng hai mảng trung gian, mỗi mảng có độ dài N: một mảng chứa tổng các hàng và một mảng chứa tổng các cột.
Đây là một ví dụ đơn giản về những gì tôi đang cố gắng làm (chương trình conphương pháp tiếp cận trực tiếp()
), và cách sử dụng các mảng trung gian (chương trình conphương pháp tiếp cận tinh tế()
) để đạt được kết quả tương tự:
#include
#include
hằng số int N = 7;
hằng số float giá trị đầu vào[N] = { 3.0F, 5.0F, 7.0F, 11.0F, 13.0F, 17.0F, 23.0F };
float P[N][N]; // Đúng vậy, tôi đang lãng phí một nửa mảng. Theo cách này, tôi không phải bận tâm đến việc ánh xạ các chỉ số.
kết quả float1[N] = { 0,0F, 0,0F, 0,0F, 0,0F, 0,0F, 0,0F, 0,0F };
kết quả float2[N] = { 0,0F, 0,0F, 0,0F, 0,0F, 0,0F, 0,0F, 0,0F };
float f(float arg1, float arg2)
{
// Tính toán tùy ý
trả về (arg1 * arg2);
}
float compute_result(int index)
{
float row_sum = 0.0F;
float col_sum = 0,0F;
hàng int;
int cột;
// Tính tổng hàng
đối với (col = (chỉ số + 1); col < N; col++)
{
row_sum += P[chỉ số][cột];
}
// Tính tổng cột
đối với (hàng = 0; hàng < chỉ số; hàng++)
{
col_sum += P[hàng][chỉ số];
}
trả về (tổng_hàng - tổng_cột);
}
void direct_approach()
{
hàng int;
int cột;
đối với (hàng = 0; hàng < N; hàng++)
{
đối với (cột = (hàng + 1); cột < N; cột++)
{
P[hàng][cột] = f(giá_trị_đầu_vào[hàng], giá_trị_đầu_vào[cột]);
}
}
int chỉ số;
đối với (chỉ số = 0; chỉ số < N; chỉ số++)
{
result1[index] = compute_result(index);
}
}
void refine_approach()
{
float row_sums[N];
float col_sums[N];
int chỉ số;
// Khởi tạo mảng trung gian
đối với (chỉ số = 0; chỉ số < N; chỉ số++)
{
row_sums[chỉ số] = 0,0F;
col_sums[chỉ số] = 0,0F;
}
// Tính tổng hàng và cột
// Điều này có thể được song song hóa bằng cách tính tổng hàng và cột
// độc lập, thay vì trong các vòng lặp lồng nhau.
hàng int;
int cột;
đối với (hàng = 0; hàng < N; hàng++)
{
đối với (cột = (hàng + 1); cột < N; cột++)
{
float được tính = f(giá trị_đầu_vào[hàng], giá_trị_đầu_vào[cột]);
row_sums[hàng] += đã tính toán;
col_sums[col] += đã tính toán;
}
}
// Tính toán kết quả
đối với (chỉ số = 0; chỉ số < N; chỉ số++)
{
result2[index] = row_sums[index] - col_sums[index];
}
}
void print_result(int n, float * kết quả)
{
int chỉ số;
đối với (chỉ số = 0; chỉ số < n; chỉ số++)
{
printf(" [%d]=%f\n", chỉ mục, kết quả[chỉ mục]);
}
}
int main(int argc, char * * argv)
{
printf("Kiểm tra giảm dữ liệu\n");
tiếp cận trực tiếp();
printf("Kết quả 1:\n");
in_kết_quả(N, kết_quả1);
phương pháp tiếp cận tinh tế();
printf("Kết quả 2:\n");
in_kết_quả(N, kết_quả2);
trả về (0);
}
Việc song song hóa quá trình tính toán không hề dễ dàng vì mọi giá trị trung gian đều là hàm của hầu hết các đầu vào. Bạn có thể tính tổng riêng biệt, nhưng điều đó có nghĩa là phải thực hiện f(...) nhiều lần. Đối với các giá trị N rất lớn, gợi ý tốt nhất mà tôi có thể nghĩ đến là sử dụng nhiều mảng trung gian hơn, tính toán các tập hợp con của kết quả, sau đó tính tổng các mảng một phần để có được tổng cuối cùng. Khi tôi không quá mệt mỏi, tôi phải nghĩ về điều đó.
Để giải quyết vấn đề bỏ qua: Nếu chỉ là "không sử dụng các giá trị đầu vào x, y và z", thì bạn có thể lưu trữ x, y và z trong một mảng do_not_use và kiểm tra tổng các giá trị đó khi bạn lặp lại các phép tính. Nếu các giá trị bạn muốn bỏ qua là một số hàm của hàng và cột, bạn có thể lưu trữ chúng theo cặp và kiểm tra các cặp.
Hy vọng điều này sẽ giúp bạn tìm ra giải pháp!
Cập nhật, bây giờ tôi đã hiểu rõ hơn:Việc xử lý "bỏ qua" phụ thuộc rất nhiều vào dữ liệu cần bỏ qua. Một khả năng khác cho trường hợp đầu tiên - "không sử dụng các giá trị đầu vào x, y và z" - một giải pháp nhanh hơn cho các tập dữ liệu lớn là thêm một cấp độ gián tiếp: tạo một mảng khác có chỉ mục là số nguyên và chỉ lưu trữ các chỉ mục của các đầu vào tốt. Trong ví dụ thứ hai, nếu đầu vào 2 và 5 chứa dữ liệu không hợp lệ, mảng hợp lệ là:
int chỉ số_hợp_lệ[] = { 0, 1, 3, 4, 6 };
Lặp lại trên một mảng
chỉ số hợp lệ
và sử dụng các chỉ số này để lấy dữ liệu từ mảng đầu vào để tính toán kết quả. Mặt khác, nếu giá trị bị bỏ qua phụ thuộc vào hai chỉ số của mảng P, thì tôi không thấy cách nào để tránh một số loại tra cứu.
Quay lại song song hóa - dù sao thì bạn cũng sẽ xử lý các phép tính (N**2-N)/2
f(). Một khả năng là chỉ chấp nhận tranh chấp về tổng số
mảng, nếu tính toán f() mất nhiều thời gian hơn
Hai sự gia tăng này. Khi bạn đạt đến một số lượng lớn các đường song song, sự cạnh tranh sẽ
Vẫn là một vấn đề, nhưng cần phải có một "điểm ngọt" để cân bằng số lượng song song
Tính thời gian cần thiết cho f().
Nếu vẫn còn tranh chấp, có một số cách để giải quyết vấn đề. Một phương pháp là
Tính toán từng hàng hoặc từng cột một: Đối với từng hàng hoặc từng cột một, tổng của mỗi cột có thể là
Được tính toán độc lập và có thể giữ tổng số liên tục cho mỗi hàng.
Một cách tiếp cận khác là chia không gian dữ liệu thành
Các tập hợp con, trong đó mỗi tập hợp con có một mảng tổng hàng và tổng cột riêng. Sau mỗi khối
Sau khi tính toán, các mảng độc lập có thể được cộng lại để tạo ra giá trị bạn cần
Tính kết quả.
Tôi là một lập trình viên xuất sắc, rất giỏi!