본문 바로가기
프로그램

[파이썬] 1,875배 빨라진다! 조건에 맞는 데이터 계산(Numba GPU 사용)

by 오디세이99 2025. 6. 8.
728x90
반응형

 

아래와 같이 실행속도를 더 빠르게 하는 방법을 진행해 보고 있습니다.

 

https://question99.tistory.com/1095

 

[파이썬] 조건에 맞는 데이터 계산(다중 for문)

어떤 데이터가 있을때 어떤 컬럼의 데이터들끼리 어떤 조건들로 찾거나 계산할때import timeimport pandas as pdimport numpy as npnp.random.seed(0) # 난수 생성 시 일관성을 위해 시드 설정### 1) 데이터 생성data_s

question99.tistory.com

 

https://question99.tistory.com/1096

 

[파이썬] 12배 빨라진다! 조건에 맞는 데이터 계산(Parallel, 병렬처리, 다중프로세스)

https://question99.tistory.com/1095 [파이썬] 조건에 맞는 데이터 계산(다중 for문)어떤 데이터가 있을때 어떤 컬럼의 데이터들끼리 어떤 조건들로 찾거나 계산할때import timeimport pandas as pdimport numpy as npnp.r

question99.tistory.com

 

https://question99.tistory.com/1097

 

[파이썬] 150배 빨라진다! 조건에 맞는 데이터 계산(병렬처리와 Numba사용)

https://question99.tistory.com/1095 [파이썬] 조건에 맞는 데이터 계산(다중 for문)어떤 데이터가 있을때 어떤 컬럼의 데이터들끼리 어떤 조건들로 찾거나 계산할때import timeimport pandas as pdimport numpy as npnp.r

question99.tistory.com

 

 

https://question99.tistory.com/1098

 

[파이썬] 680배 빨라진다! 조건에 맞는 데이터 계산(병렬처리 하지 않고 Numba만 사용)

https://question99.tistory.com/1095 [파이썬] 조건에 맞는 데이터 계산(다중 for문)어떤 데이터가 있을때 어떤 컬럼의 데이터들끼리 어떤 조건들로 찾거나 계산할때import timeimport pandas as pdimport numpy as npnp.r

question99.tistory.com

 

 

Numba를 사용해서 기존에는 불가능했던 처리까지 할 수 있게 되었습니다.

그러나 이런 가능성이 생기니까 더 큰 환경에서 실행해보게 되었습니다.

그래서 Numba에서 GPU를 사용하는 방법을 찾아보았습니다.

 

import time
import pandas as pd
import numpy as np
import itertools
from numba import cuda # Numba CUDA 임포트

np.random.seed(0) # 난수 생성 시 일관성을 위해 시드 설정

### 1) 데이터 생성
data_size = 2000
col_names = ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10'
             # ,'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18', 'c19', 'c20'
            ]
data = {
    col: np.random.randint(50, 201, size=data_size) for col in col_names
}

# DataFrame 생성
df = pd.DataFrame(data)

### 2) 조건의 조합(순열) 만들기
lst = list(itertools.permutations(col_names, 2)) # 순열 만들기
# lst_no는 이제 GPU로 전달될 숫자 인덱스 튜플의 NumPy 배열이 됩니다.
lst_no_numeric = []
for item in lst:
    n1 = col_names.index(item[0]) # 해당 컬럼명(문자열)의 인덱스를 찾아 정수로 사용
    n2 = col_names.index(item[1])
    lst_no_numeric.append((n1, n2))

# Python list of tuples -> NumPy array of tuples (for GPU)
# GPU에 전달하기 위해 (num_permutations, 2) 형태의 2D 배열로 만듭니다.
lst_no_array = np.array(lst_no_numeric, dtype=np.int32)


# GPU 커널 정의
@cuda.jit
def run_simulation_gpu_kernel(lst_no_gpu, n_df_gpu, output_max_counts_gpu, output_indices_gpu, output_cases_gpu):
    # 각 스레드가 처리할 조합의 인덱스를 계산 (여기서는 item1, item2의 조합 인덱스)
    # 총 조합의 개수는 lst_no_gpu의 첫 번째 차원 크기의 제곱
    total_permutations = lst_no_gpu.shape[0]
    
    # 각 GPU 스레드는 (item1, item2) 조합 중 하나를 처리합니다.
    # 즉, outer_loop_idx = item1의 인덱스, inner_loop_idx = item2의 인덱스
    # 1D 그리드를 2D 논리적 인덱스로 매핑합니다.
    # 각 스레드는 item1과 item2의 모든 조합 중 하나를 처리합니다.
    # 예를 들어, 스레드 ID가 0, 1, 2, ... 일 때,
    # (0,0), (0,1), ..., (0,N), (1,0), (1,1), ... 처럼 매핑할 수 있습니다.
    
    # 2D 인덱스를 1D 스레드 ID에 매핑
    # (total_permutations * total_permutations) 만큼의 스레드가 필요합니다.
    thread_id = cuda.grid(1) # 현재 스레드의 전역 ID

    # 각 스레드가 처리할 item1_idx와 item2_idx를 계산
    if thread_id < total_permutations * total_permutations:
        item1_idx = thread_id // total_permutations # item1의 인덱스
        item2_idx = thread_id % total_permutations # item2의 인덱스

        # 실제로 사용할 item1과 item2의 컬럼 번호 튜플
        item1_col_idx1 = lst_no_gpu[item1_idx, 0]
        item1_col_idx2 = lst_no_gpu[item1_idx, 1]
        item2_col_idx1 = lst_no_gpu[item2_idx, 0]
        item2_col_idx2 = lst_no_gpu[item2_idx, 1]

        count1_local = 0 # 각 스레드 내의 지역 변수
        count2_local = 0 # 각 스레드 내의 지역 변수

        # 데이터의 row별 조건 확인을 위한 반복문 (GPU 스레드 내에서 순차적으로 실행)
        for i in range(n_df_gpu.shape[0]):
            data1 = n_df_gpu[i, item1_col_idx1]
            data2 = n_df_gpu[i, item1_col_idx2]
            data3 = n_df_gpu[i, item2_col_idx1]
            data4 = n_df_gpu[i, item2_col_idx2]

            if data1 > data2:
                count1_local += 1
            elif data3 > data4:
                count2_local += 1

        count_sum_local = count1_local + count2_local

        # 결과를 미리 할당된 출력 배열에 저장
        # 각 스레드는 자신의 thread_id에 해당하는 인덱스에 결과를 씁니다.
        output_max_counts_gpu[thread_id] = count_sum_local
        output_indices_gpu[thread_id] = thread_id # 이 스레드의 고유 ID 또는 매핑된 idx (idx는 여기서는 thread_id)
        
        # max_case1과 max_case2는 (item1_col_idx1, item1_col_idx2) 형태로 저장
        output_cases_gpu[thread_id, 0, 0] = item1_col_idx1
        output_cases_gpu[thread_id, 0, 1] = item1_col_idx2
        output_cases_gpu[thread_id, 1, 0] = item2_col_idx1
        output_cases_gpu[thread_id, 1, 1] = item2_col_idx2

# n_df를 NumPy 배열로 변환
n_df_array = df[col_names].values.astype(np.int32) # 데이터 타입을 명확히 지정
n_df_gpu = cuda.to_device(n_df_array) # 데이터를 GPU로 전송

# lst_no_numeric를 GPU로 전송
lst_no_gpu = cuda.to_device(lst_no_array)

# GPU 커널 실행을 위한 설정
total_combinations = lst_no_array.shape[0] * lst_no_array.shape[0] # 전체 (item1, item2) 조합의 개수

threads_per_block = 256 # 블록당 스레드 수 (튜닝 가능)
blocks_per_grid = (total_combinations + (threads_per_block - 1)) // threads_per_block

# 결과를 저장할 GPU 메모리 배열 할당
# 각 스레드가 하나의 결과를 저장
output_max_counts_gpu = cuda.device_array(total_combinations, dtype=np.int32)
output_indices_gpu = cuda.device_array(total_combinations, dtype=np.int32)
# max_case1, max_case2를 저장하기 위한 배열 (각각 2개의 인덱스를 가짐)
output_cases_gpu = cuda.device_array((total_combinations, 2, 2), dtype=np.int32) # (조합수, 2개 케이스, 각 케이스 2개 인덱스)

print(f"총 {total_combinations:,}개의 조합을 GPU에서 계산합니다.")
print(f"그리드 크기: {blocks_per_grid}, 블록당 스레드: {threads_per_block}")

# start_time = time.time()
start_time_pc = time.perf_counter()

# GPU 커널 실행
run_simulation_gpu_kernel[blocks_per_grid, threads_per_block](
    lst_no_gpu,
    n_df_gpu,
    output_max_counts_gpu,
    output_indices_gpu,
    output_cases_gpu
)
cuda.synchronize() # GPU 작업이 완료될 때까지 대기

# GPU에서 CPU로 결과 가져오기
all_counts = output_max_counts_gpu.copy_to_host()
all_indices = output_indices_gpu.copy_to_host()
all_cases = output_cases_gpu.copy_to_host()

# CPU에서 최종 Best case 찾기 (리덕션)
max_count = 0
best_idx = 0
best_case1 = []
best_case2 = []

for i in range(total_combinations):
    current_count = all_counts[i]
    if current_count > max_count:
        max_count = current_count
        best_idx = all_indices[i] # 또는 i (현재 스레드 ID)
        best_case1 = all_cases[i, 0, :].tolist() # NumPy 배열을 리스트로 변환
        best_case2 = all_cases[i, 1, :].tolist() # NumPy 배열을 리스트로 변환

### 4) 출력
print(f"Best case : {max_count:,} (idx={best_idx})")
print(f"Best case : {max_count:,} (idx={best_idx}), ({col_names[best_case1[0]]},{col_names[best_case1[1]]}):({col_names[best_case2[0]]},{col_names[best_case2[1]]})")

# 실행기간 계산 및 출력
# end_time = time.time()
# elapsed = end_time - start_time
# minutes = int(elapsed // 60)
# seconds = int(elapsed % 60)
# print(f"\nRunning time: {minutes}m {seconds}sec")
# end_time_pc = time.perf_counter()
# gpu_elapsed_time = end_time_pc - start_time_pc
# print(f"\nGPU Kernel Running time: {gpu_elapsed_time:.6f} seconds")

# CPU에서 최종 집계 시간도 별도로 측정 가능
start_cpu_post_processing = time.perf_counter()
# ... (CPU에서 최종 Best case 찾는 루프) ...
end_cpu_post_processing = time.perf_counter()
cpu_elapsed_time = end_cpu_post_processing - start_cpu_post_processing
print(f"CPU Post-processing time: {cpu_elapsed_time:.6f} seconds")

total_elapsed_time = end_cpu_post_processing - start_time_pc
print(f"Total end-to-end Running time: {total_elapsed_time:.6f} seconds")

처음 다중for 문의 300 sec에서 지금 0.16sec로

1,875배

빨리 실행하는 방법을 찾을 수 있습니다.

 

위 코드를 실행하면 다음과 같이 Warning이 보입니다. 이것은 실행시간이 빨라서 GPU를 사용시간이 작다는 것입나다.

조건을 더 늘리면 이 Warning은 보이지 않게 됩니다.

numba\cuda\dispatcher.py:536: NumbaPerformanceWarning: Grid size 32 will likely result in GPU under-utilization due to low occupancy.
  warn(NumbaPerformanceWarning(msg))
728x90
반응형

댓글