harmdevries's picture
Update app.py
6c2da96
raw
history blame
8.74 kB
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)