import streamlit as st st.sidebar.header("Transformer parameters") col1, col2 = st.sidebar.columns([2, 4]) bs = st.sidebar.number_input('Batch size', value=10) h = st.sidebar.number_input('Num heads',value=16) d = st.sidebar.number_input('Dimension', value=768) l = st.sidebar.number_input('Num layers', value=24) n_start = st.sidebar.number_input('Start seq', value=1) n = st.sidebar.number_input('End seq', value=1024) st.sidebar.header("GPU parameters") GPU = st.sidebar.selectbox('GPU', ('A100', 'V100')) if GPU == 'A100': # A100 specs TFLOPS = 312e12 GB_S = 1935e9 elif GPU == 'V100': TFLOPS = 112e12 GB_S = 900e9 else: raise ValueError('Unknown GPU') # in ms THREAD_OVERHEAD = st.sidebar.number_input('Thread overhead (in ms)', format="%.3f", value=0.005) GPU_EFFICIENCY = st.sidebar.number_input('GPU efficiency', format="%.3f", value=0.5) TFLOPS = GPU_EFFICIENCY*TFLOPS # in ms def calc_exec_time(comp_flop, mem_bytes, include_overhead=True): exec_time = max(comp_flop/TFLOPS, mem_bytes/GB_S) exec_time *= 1000 if include_overhead: exec_time = max(exec_time, THREAD_OVERHEAD) return exec_time def qkv_mha_exec(bs, h, n, d): flop = 2*bs*1*d*3*d nbytes = 2*bs*1*d + 2*3*d*d + 2*bs*1*3*d exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def qkv_mqa_exec(bs, h, n, d): flop = 2*bs*1*d*(1+2/h)*d nbytes = 2*bs*1*d + 2*(2/h)*d*d + 2*bs*1*(2/h)*d exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def att1_mha_exec(bs, h, n, d): flop = 2*bs*h*(d/h)*n nbytes = 2*bs*h*(d/h) + 2*bs*h*n*(d/h) + 2*bs*h*n exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def att1_mqa_exec(bs, h, n, d): flop = 2*bs*h*(d/h)*n nbytes = 2*bs*h*(d/h) + 2*bs*n*(d/h) + 2*bs*h*n exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def att2_mha_exec(bs, h, n, d): flop = 2*bs*h*n*(d/h) nbytes = 2*bs*h*n + 2*bs*h*n*(d/h) + 2*bs*h*(d/h) exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def att2_mqa_exec(bs, h, n, d): flop = 2*bs*h*n*(d/h) nbytes = 2*bs*n*(d/h) + 2*bs*n*(d/h) + 2*bs*h*(d/h) exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def out_exec(bs, h, n, d): flop = 2*bs*1*d*d nbytes = 2*bs*1*d + 2*d*d + 2*bs*1*d exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def softmax_exec(bs, h, n, d): flop = 0 nbytes = 2*bs*h*n + 2*bs*h*n exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def ln_exec(bs, h, n, d): nbytes = 2*bs*1*d + 2*bs*1*d flop = 0 exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def mlp_exec(bs, h, n, d): flop = 2*bs*1*d*4*d nbytes = 2*bs*1*d + 2*d*4*d + 2*bs*1*4*d exec_time = calc_exec_time(flop, nbytes) return flop, nbytes, exec_time def print_kernel_execution(flop, nbytes): c1, c2 = st.columns([2, 3]) exec_time = calc_exec_time(flop, nbytes, include_overhead=False) flop = round(flop/1e9, 2) nbytes = round(nbytes/1e6, 2) c1.write("GFLOP:") c2.write(str(flop)) c1.write("MB: ") c2.write(str(nbytes)) c1.write("Time (ms):") c2.write(str(exec_time)) c1.write("Overhead (ms):") c2.write(str(THREAD_OVERHEAD)) st.title("Inference time MHA vs MQA") st.write("This space approximates the inference time for Multi-Query Attention and Multi-Head Attention transformers. You can change the hyperparameters in sidebar.") mqa_total_time = 0. mha_total_time = 0. for i in range(n_start, n): shared_time = out_exec(bs, h, i, d)[2] + softmax_exec(bs, h, i , d)[2] + 2*ln_exec(bs, h, i, d)[2] \ + 2*mlp_exec(bs, h, i, d)[2] + 3*ln_exec(bs, h, i, d)[2] mha_time = shared_time + qkv_mha_exec(bs, h, i, d)[2] + att1_mha_exec(bs, h, i, d)[2] + att2_mha_exec(bs, h, i, d)[2] mha_total_time += l*mha_time mqa_time = shared_time + qkv_mqa_exec(bs, h, i, d)[2] + att1_mqa_exec(bs, h, i, d)[2] + att2_mqa_exec(bs, h, i, d)[2] mqa_total_time += l*mqa_time c1, c2 = st.columns([2, 4]) c1.write("Multi-Head Attention:") c2.write(str(round(mha_total_time, 2))) c1.write("Multi-Query Attention:") c2.write(str(round(mqa_total_time, 2))) c1.write("Speed-up MQA over MHA:") c2.write(str(round(mha_total_time/mqa_total_time,2))) st.subheader("Memory consumption") st.caption("Multi-Head Attention") c1, c2 = st.columns([2, 4]) num_params = 12*l*d*d c1.write("Num Parameters (in B)") c2.write(str(round(num_params/1e9, 3))) c1.write("Stored Parameters (GB)") c2.write(str(round(2*num_params/1e9, 3))) c1.write("Cached keys and values (GB)") acts = round(2*bs*l*(d/h)*h*2*n/1e9, 2) c2.write(str(acts)) st.caption("Multi-Query Attention") c1, c2 = st.columns([2, 4]) num_params = (10+2/h)*l*d*d c1.write("Num Parameters (in B)") c2.write(str(round(num_params/1e9, 3))) c1.write("Stored Parameters (GB)") c2.write(str(round(2*num_params/1e9, 3))) c1.write("Cached keys and values (GB)") acts = round(2*bs*l*(d/h)*2*n/1e9, 2) c2.write(str(acts)) st.subheader("Estimating execution time") st.markdown("We use the [following crude approximation](https://docs.nvidia.com/deeplearning/performance/dl-performance-gpu-background/index.html#understand-perf) to estimate the execution time for each matrix multiplication.") st.latex("C = A \cdot B") st.latex("A \in \mathbb{R}^{MxK}, B \in R^{KxN}, C \in \mathbb{R}^{MxN}") st.markdown(''' To execute this operation on the GPU, we need to 1. Read A, B from memory 2. Perform matrix multiplication 3. Write C to memory ''') st.markdown("For float16 operations (2 bytes), we can estimate the memory access time of A as follows:") st.latex("T_{mem}(A) = 2*M*K / BW_{mem}") st.markdown("where BW_mem is the memory bandwidth of the GPU (e.g. 1935 GB/s for an A100 GPU)") st.markdown("The total time on memory access is T_mem = T_mem(A) + T_mem(B) + T_mem(C)") st.markdown("We can estimate the compute time for the math operations as follows:") st.latex("T_{math}(A \cdot B) = 2*M*K*N / BW_{math}") st.markdown("where BW_math is the number of floating point operations per second (e.g. 312 TFLOPS for an A100 GPU)") st.markdown("If we assume we can *perfectly* overlap memory access with math operations, then the estimated execution time for the operation is:") st.latex("max(T_{math}, T_{mem})") st.markdown("Note that there is a minimum time to execute the operation due to [kernel launch overhead](https://forums.developer.nvidia.com/t/any-way-to-measure-the-latency-of-a-kernel-launch/221413/2)") st.subheader("Inference time for Transformer operations") st.markdown("We can now estimate the execution for each of the operations in the transformer model. I suggest you inspect the code for details on the calculations. ") st.subheader('Attention layer') st.markdown('**QKV projection**') st.caption("Multi-Head Attention") flop, nbytes, exec_time = qkv_mha_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.caption("Multi-Query Attention") flop, nbytes, exec_time = qkv_mqa_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.markdown('**QK gemm**') st.write("Showing calculation for the maximum sequence length (n)") st.caption("Multi-Head Attention") flop, nbytes, exec_time = att1_mha_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.caption("Multi-Query Attention") flop, nbytes, exec_time = att1_mqa_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.markdown('**Attention-value gemm**') st.write("Showing calculation for the maximum sequence length (n)") st.caption("Multi-Head Attention") flop, nbytes, exec_time = att2_mha_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.caption("Multi-Query Attention") flop, nbytes, exec_time = att2_mqa_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.markdown('**Output projection**') flop, nbytes, exec_time = out_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.markdown('**Element-wise ops**') st.write("We also need to take into the softmax layer, layer norm, and residual connection. We assume that these operations are memory bound. ") st.caption("Softmax") flop, nbytes, exec_time = softmax_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.caption("Layer norm/residual connection") flop, nbytes, exec_time = ln_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.subheader('MLP layer') st.markdown('**First and Second Linear Layer**') flop, nbytes, exec_time = mlp_exec(bs, h, n, d) print_kernel_execution(flop, nbytes) st.markdown('**Element-wise ops**') st.write("We also need to take into the GeLU, layer norm, and residual connection. We assume that these operations are memory bound. ") flop, nbytes, exec_time = ln_exec(bs, h, n, d) print_kernel_execution(flop, nbytes)