Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Optimize LlamaAttention by fusing QKV projections#40092

Open
null-pointer-access wants to merge 1 commit into
huggingface:mainfrom
null-pointer-access:optimize-llama-attention-qkv-fusion
Open

Optimize LlamaAttention by fusing QKV projections#40092
null-pointer-access wants to merge 1 commit into
huggingface:mainfrom
null-pointer-access:optimize-llama-attention-qkv-fusion

Conversation

@null-pointer-access

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR fuses the q, k, and v projections in LlamaAttention to a single qkv projection. This can improve the efficiency and GPU utilization when the batch size is small. Specifically, when running a single prefill with seq_len=128 on 1xH100, it achieves 28% efficiency improvement.

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you read the contributor guideline,
    Pull Request section?
  • Was this discussed/approved via a Github issue or the forum? Please add a link
    to it if that's the case.
  • Did you make sure to update the documentation with your changes? Here are the
    documentation guidelines, and
    here are tips on formatting docstrings.
  • Did you write any new necessary tests?

Who can review?

Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@ArthurZucker

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: llama

@ArthurZucker ArthurZucker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey!
Thanks but we cannoit accept this.

Specifically, when running a single prefill with seq_len=128 on 1xH100, it achieves 28% efficiency improvement.

Well, this needs benchmarking, making sure it holds with compile, with cudagraphs, with fa2 kernels, etc

@null-pointer-access

Copy link
Copy Markdown
Contributor Author

@ArthurZucker Thanks for the feedback! Actually, this implementation is inspired by

qkv = self.qkv_proj(hidden_states)

which replaces multiple operator calls (q_proj, k_proj, v_proj) with a single fused operator (qkv_proj). To show that this fix can provide stable speedup under different workloads, we did extensive evaluations and here's the result with TinyLlama-1.1B-Chat-v1.0 on an H100 GPU:

  • Different batch sizes (seq_len=128, compile=False, CUDA graphs=False):
Batch size q_proj (us) k_proj (us) v_proj (us) Baseline q_proj+k_proj+v_proj (us) Fused qkv_proj (us) Latency reduction
1 7.522 5.666 5.762 18.950 8.802 53.6%
4 9.282 6.370 5.922 21.574 10.434 51.6%
8 14.306 7.074 6.562 27.942 18.242 34.7%
16 24.930 7.904 7.714 40.548 31.010 23.5%
  • Different Sequence lengths (batch_size=4, compile=False, CUDA graphs=False):
Seq len q_proj (us) k_proj (us) v_proj (us) Baseline q_proj+k_proj+v_proj (us) Fused qkv_proj (us) Latency reduction
256 14.306 6.850 6.594 27.750 18.498 33.4%
512 24.994 8.162 7.714 40.870 30.529 25.3%
1024 45.922 11.330 9.186 66.438 58.946 11.3%
  • With torch.compile:
Batch size Seq len E2E throughput (q_proj, k_proj, v_proj) [token/s] E2E throughput (fused qkv_proj) [token/s] Relative gain
1 128 48467.009 54143.996 +11.7%
8 512 159668.952 161672.855 +1.3%
  • With CUDA graphs:
Batch size Seq len Dtype E2E throughput (q_proj, k_proj, v_proj) [token/s] E2E throughput (fused qkv_proj) [token/s] Relative gain
4 256 float16 113188.848 115958.794 +2.4%

You can find the benchmark script here.

Comment on lines +229 to +233
qkv = self.qkv_proj(hidden_states)
query_pos = self.config.num_attention_heads * self.head_dim
query_states = qkv[..., :query_pos]
key_states = qkv[..., query_pos : query_pos + self.config.num_key_value_heads * self.head_dim]
value_states = qkv[..., query_pos + self.config.num_key_value_heads * self.head_dim :]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! I agree that fusing the linear layers for Q, K, and V should generally be faster than using three separate ones. However, there's a potential consequence to consider.

This fusion will result in the q, k, v tensors not being contiguous in memory. As a result, the attention mechanism will need to call contiguous() on them afterward, which involves copying the non-contiguous q, k, v tensors to a new contiguous memory location. This copying could introduce a latency and performance trade-off that should be evaluated in benchmarks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ducviet00 , thanks for your feedback! I looked into the problem you mentioned and did some benchmark, and find that there would not be additional contiguous() because in LlamaModel, these tensors will then be forced to be contiguous by torch.cat or repeat_kv.

def rotate_half(x):
"""Rotates half the hidden dims of the input."""
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)

def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
"""
This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
"""
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
if n_rep == 1:
return hidden_states
hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nice

@ArthurZucker ArthurZucker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @null-pointer-access 🤗
It's a very well well known "issue" in the sense that it can be faster but as your benchmarks show, it is not obvious!
If you think about inference: cudagraph + compile make it ~same perf
If you think about trining: the fact that you split q, k, v make it a lost simpler to run PEFT (LORA).
And more than that, its impossible to change the format now, because it would break the 300K models on the hub 😉

There is the deprecated/open_llama that tried to push the performances, but TBH it does not get usage.

I am not convinced that it is worth it expecially for all torch dtype / compile / cudagraph / tensor parallel etc!

@ParagEkbote

Copy link
Copy Markdown
Contributor

Not to chime into a conversation, but perhaps you could package the changes in the PR into a cookbook-style recipe here: https://github.com/huggingface/cookbook

The recipe could document how LlamaAttention or any other module can be speed up and be modified for use in transformers, for a particular use-case.

cc: @null-pointer-access

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants