-
Notifications
You must be signed in to change notification settings - Fork 10.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
llama.vim : plugin for Neovim #9787
Conversation
73fa77d
to
391ea30
Compare
An updated version will be added in #9787
32da4a2
to
3681540
Compare
76e5d87
to
cefd4ac
Compare
c7d8904
to
d2c559a
Compare
5155b68
to
acf6d19
Compare
This plugin (or script?) was quite fun to implement! Will be merging after a few days of testing. If anyone gives this a try, would be happy to hear any feedback. This is running pretty smooth on M2 Ultra with Qwen2.5 7B Q8, though I think it should work reasonably well even on lower end hardware. |
ggml-ci
You explicitly state neovim, but is there anything you use that prevents the use of vim? |
As far as I know, the async job and virtual text APIs are a bit different in Vim. Though it's probably quite easy to adapt the script to work both with Vim and Neovim. |
'eol' messes up the rendering with nvim v0.10.2 for some reason
@Green-Sky I have a treat for you in #9995 |
The Further development will continue in the https://github.com/ggml-org/llama.vim repo. |
An updated version will be added in ggerganov#9787
'eol' messes up the rendering with nvim v0.10.2 for some reason
An updated version will be added in ggerganov#9787
'eol' messes up the rendering with nvim v0.10.2 for some reason
An updated version will be added in ggerganov#9787
'eol' messes up the rendering with nvim v0.10.2 for some reason
ref ggml-org/p1#1
The plugin is now developed here: https://github.com/ggml-org/llama.vim
Overview
Add a simple Neovim plugin for local LLM-assisted code/text completion.
Features
Insert
modeCtrl+F
Tab
Shift+Tab
Usage
Setup a
llama-server
instance with a FIM-compatible model (RoPE required). For example:works best with Qwen2.5-Coder models (not Instruct)
Copy or symlink examples/llama.vim to
~/.config/nvim/autoload/llama.vim
Start Neovim and run:
For more advanced options, check the parameters in
g:llama_config
in examples/llama.vim:llama.cpp/examples/llama.vim
Lines 43 to 86 in acf6d19
Sample configs based on hardware
High-end hardware with GPU
Mid-end hardware with GPU
Low-end hardware with GPU
Low-end hardware (CPU only)
Backend changes
Debugging
llama-server .. -lv 1
GGML_DEBUG_SAMPLER_INFILL
inllama-sampling.cpp
Technical details
The plugin uses the
/infill
endpoint of thellama-server
. It sends asynchronous FIM requests to the server via thecurl
tool:The
"input_prefix"
and"input_suffix"
are constructed by picking nearby lines around the cursor location:The
"prompt"
is set as the text to the left of the cursor on the current line:So far this is very a standard FIM completion using "local" context. Adding more and more context will usually improve the quality of the completion, but it will also increase the latency. As a datapoint, consider that a 7B LLM running on a 76 core M2 Ultra GPU roughly takes ~1 second to process 1000 tokens of context. Modern LLMs have training contexts of more than 32k tokens, so filling the entire context with local context and reprocessing it on each completion request is obviously not feasible for local completion, as it would be exceedingly slow. For good user experience, we aim at a latency of about ~1 second or less per completion suggestion, while utilizing the full context of the model at the same time. Read more on how we solve this problem further down the text.
Global context
In addition to the local context around the current cursor location, we can significantly improve the quality of the generated suggestions by including extra "global" context. This extra context can come either from other places in the same file that we are currently editing, or from other recently edited or opened files. There are a lot of different techniques for deciding which extra context specifically to include in the request that could be potentially relevant to the current completion task. In the
llama.vim
plugin, we use a simple approach:g:llama_config.ring_n_chunks
chunks ofg:llama_config.ring_chunk_size
lines eachg:llama_config.ring_scope
lines around the cursor)Upon each FIM completion request, we now send both the local and global contexts together. The latter is passed through the
"input_extra"
field of the/infill
request in the following format:With this design, as we edit the files in our Neovim session, the overall context grows to a certain amount (determined by the ring buffer size) and usually contains up-to-date relevant information for the editing task at hand. The specific events and logic for gathering chunks can be easily modified and customized if needed.
Note that the entire state of the context is stored client-side and is sent to the server on each request.
Server-side processing
Upon receiving a request with
N
extra context chunks, the server constructs the following repo-level FIM prompt:This is based on the work in https://arxiv.org/pdf/2409.12186. Note that not all models are trained for this pattern, so it is recommended to use models that support it, such as
Qwen2.5-Coder
. This prompt format has important advantages that allow efficient context reuse, discussed in the following paragraphs.In this FIM prompt, the components correspond to:
<|repo_name|>
,<|file_sep|>
,<|fim_prefix|>
,<|fim_suffix|>
,<|fim_middle|>
- special tokens defined by the modelfilename_i
- thefilename
of the i'th chunk in the"input_extra"
arraytext_i
- thetext
of the i'th chunk in the"input_extra"
arrayprefix
,suffix
,prompt
- the input from the"input_prefix"
,"input_suffix"
, and"prompt"
fields of the requestThe server processes the constructed prompt and then generates a maximum number of tokens that represent the FIM completion. The generation can be terminated early by several different conditions:
The generated text is sent back to the client for display as a suggestion via virtual text overlay.
KV cache reuse : global prefix
The first optimization technique for improving long-context performance is to simply reuse the computed KV cache common prefix from the previous request. This allows us to very efficiently append new chunks of extra context, in-between the
<|fim_prefix|>
token and the existing chunks in the extra context:Reusing the KV cache prefix is supported generally by the
llama-server
and requires simply to provide the"cache_prompt": true
flag in the completion requests. With this option, each new completion request will reuse the largest common prefix of tokens between the old and the new request. This saves a large part of the prompt processing in situations where the extra context does not change, or was extended by appending a new chunk at the end.KV cache reuse : context shift
The previous optimization is only useful up to
g:ring_n_chunks
chunks of extra context. When the ring buffer becomes full, the first chunk would be evicted and would therefore "shift" all following chunks into a new position relative to the start of the prompt:Because of this relative shift of
D0
tokens, it is no longer possible to directly reuse the KV cache of the extra context. The reason for this is because the position of the tokens is encoded inside the KV cache data (e.g. via the RoPE operator) and now the tokens are no longer in those particular positions (for more info, see #71 (comment)).However, quite early in the project (#2060), we realized that the cache in this case can actually be efficiently reused by "updating" the encoded positions in the K cache. This follows from the observation that the RoPE operator is "additive". Roughly speaking, applying a RoPE with position
p1 = p0 + d
is equivalent to applying:p0
d
on the already RoPE'd data in the previous stepThis provides a very cheap way to "move" the remaining chunks in the ring buffer forward, towards the beginning of the context: simply apply RoPE with position
-D0
to all tokens in the K cache that we want to reuse. Doing so, we can again save the computation of a large portion of the extra prompt.Note that the described context shifting method is not mathematically identical to recomputing the entire prompt from scratch. It can be easily seen that the embeddings at each token position are "entangled" with all the embeddings before that position, so simply "shifting" the K cache positions will not produce the exact same numbers as full reprocessing. Regardless of this, the context shifting feature has been applied and used by the local
llama.cpp
community for more than an year now and empirical results indicate that it is very effective and does not seem to degrade the quality of the output in a significant way. The cache reuse techniques described here heavily rely on this "trick".The described context shifting strategy can also be applied when the evicted chunk is somewhere in the middle of the ring buffer or even if there are multiple evicted chunks at a time. A detailed description of the implementation can be found in #5793 and #9866.
This context reuse strategy requires the
llama-server
to be started with the--cache-reuse N
command-line argument. TheN
argument is the minimum size of the chunks (in number of tokens) that we will accept and shift in the KV cache for reuse purposes. The logic is that we don't want to reuse very small bits (e.g. individual tokens) from random places of the old context and instead we are interested in reusing large continuous blocks. Note that the implementation preserves the order of the reused chunks, so that a shifted chunk will never move over another chunk (i.e. reused chunks always appear in the same order to each other as when they were originally computed).Applying these two techniques, we can now efficiently update the extra context of our FIM requests by adding and evicting chunks any way the client decides. Existing chunks will not be recomputed and the server will process only new chunks that were not present in the previous request. The
llama.vim
plugging periodically updates the extra context ring buffer on the client side and sends the information to the server whenever it detects inactivity (i.e. the cursor hasn't moved for certain period of time or we are currently inNormal
mode). This makes the processing of the extra global context almost entirely seamless for the user, mitigating a huge portion of the latency in the naive approach.KV cache reuse : local prefix
Let's now focus again on the local context part of the request and explain one additional cache reuse strategy that helps to further reduce the completion latency in some typical cases. All of the following examples will assume the PSM (Prefix-Suffix-Middle) FIM pattern. Similar analysis can be made for the SPM (Suffix-Prefix-Middle) pattern which is supported via the
--spm-infill
command line argument ofllama-server
.Assume also that we are in the middle of editing a line of text and the client has already received a suggestion from the server:
Here is how the local FIM prompt looks like in more details:
From here, there are 3 typical follow-up completion requests that occur in most situations:
Same line FIM
For clarity, assume the cursor moved
{dx}
tokens to the right (moving to the left follows the same logic). The new FIM prompt would look like this:In this case the entire local prefix will be reused since it's contents and position are the same as in the previous request. This means that attempting FIM anywhere on the same line will be quite cheap and will involve recomputing only the suffix tokens.
Next line FIM
In this case, the new FIM prompt after moving to the next line, looks like this:
The old
{prefix_line_1}
line is now out of the FIM prefix scope and a new{prefix_line_P+1}
line is within the FIM prefix scope. We can reuse the cache for lines[2, P]
via context shifting, as explained earlier. So in this case, we compute only the new prefix line{prefix_line_P+1}
, together with the new FIM suffix.Prev line FIM
This case is the most cache unfriendly one. Moving a line up, the new FIM prompt will look like this:
Because we haven't computed the
{prefix_line_0}
line in the previous request, the cache reuse logic has to stop at the very start of the local FIM prompt. Therefore in this case we don't reuse any of the previous local FIM cache and we need to compute the entire local FIM prompt.Expected performance
On each FIM request, the server takes a maximum of 1 full batch of tokens from the provided local context. The prefix and suffix tokens are split in a ratio of
3:1
:llama.cpp/examples/server/server.cpp
Lines 2055 to 2062 in 32927e6
This means that for new FIM requests, there will be at most
--batch
tokens to process, while in most cases the processed tokens would be much less due to the cache reuse optimizations described above. Knowing this, we can estimate the typical performance of FIM requests using thellama-batched-bench
tool. Here are some analysis on M1 Pro and M2 Ultra usingQwen2.5-Coder
1.5B and 7B models:M1 Pro
M2 Ultra
From these numbers we can estimate the prompt processing and text generation speeds, as well as the expected FIM time at different levels of context occupation. Here we assume that the FIM request would require to process 1/4 of
--batch
tokens as prompt and generate32
tokens as suggestion:M1 Pro, LLM 1.5B, Q8_0:
expected FIM time in ms:
M2 Ultra, LLM 7B, Q8_0:
expected FIM time in ms:
Examples
Using
llama.vim
on M1 Pro (2021) withQwen2.5-Coder 1.5B Q8_0
:The orange text is the generated suggestion. The green text contains performance stats for the FIM request: the currently used context is
15186
tokens and the maximum is32768
. There are30
chunks in the ring buffer with extra context (out of64
). So far,1
chunk has been evicted in the current session and there are0
chunks in queue. The newly computed prompt tokens for this request were260
and the generated tokens were25
. It took1245 ms
to generate this suggestion after entering the letterc
on the current line.Using
llama.vim
on M2 Ultra withQwen2.5-Coder 7B Q8_0
:llama.vim-0-lq.mp4
Demonstrates that the global context is accumulated and maintained across different files and showcases the overall latency when working in a large codebase.
TODO
-np
and-c
as needed)Future ideas