model : support LongCat-Flash-Lite (ngram embeddings) - #19167
Conversation
| @@ -353,6 +357,9 @@ class llama_kv_cache_context : public llama_memory_context_i { | |||
| void set_input_kq_mask (ggml_tensor * dst, const llama_ubatch * ubatch, bool causal_attn) const; | |||
| void set_input_pos_bucket(ggml_tensor * dst, const llama_ubatch * ubatch) const; | |||
|
|
|||
| // used by ngram embeddings | |||
| std::vector<llama_token> get_last_n_tokens(size_t n, llama_pos pos, llama_seq_id seq_id) const; | |||
There was a problem hiding this comment.
I think it would be more consistent if you implement a set_input_ngrams(ggml_tensor * dst, const llama_ubatch * ubatch).
In the llama_kv_cells, instead of adding token id to llama_kv_cell_ext, consider adding a map to llama_kv_cells:
// seq_id[s][p] gives the token at position p for sequence s
std::map<llama_pos, llama_token> seq_id[LLAMA_MAX_SEQ];It will be very similar to the existing seq_pos map and updated in very similar way. It might be a bit tricky to handle cases with multiple tokens in the same position (see the comment for seq_pos), but not very important for now.
Also, might have to extend the pos_set(uint32_t i, llama_pos p) to set the token too:
void pos_set(uint32_t i, llama_pos p, llama_token id);Once you have this map, it's trivial to construct the ngrams. You can use this in the new set_input_ngrams method.
There was a problem hiding this comment.
Looking a bit more, its probably best to extend the pos concept a little bit like so:
Currently we have this:
llama.cpp/src/llama-kv-cells.h
Lines 463 to 468 in eef375c
New version:
struct llama_kv_cell {
llama_pos pos;
llama_token id;
};
...
// cell data (replaced the old `pos` vector)
std::vector<llama_kv_cell> data;
// stores extra info per cell
std::vector<llama_kv_cell_ext> ext;And then pos_set() becomes:
void pos_set(uint32_t i, llama_kv_cell c);
// or
void cell_set(uint32_t i, llama_kv_cell c);etc.
There was a problem hiding this comment.
Thanks! Unfortunately I just spent the whole working day to realize that it's currently not possible to implement the non-ngram version with existing GGML ops: #19182
The ngram model is just the normal one + extra embeddings, so currently I'm hitting a roadblock before I can move on with this PR. But I'll come back to this if any other models also implement ngram embeddings.
There was a problem hiding this comment.
For my understanding, the normal (non-ngram) model is discussed in #19182 and I understand the difficutly.
But this sentence:
The ngram model is just the normal one + extra embeddings, so currently I'm hitting a roadblock before I can move on with this PR.
What do you mean here by roadblock for the ngram model?
Edit: ah, nvm. I understand now - it also needs the zero-compute experts.
There was a problem hiding this comment.
I started the PR with the assumption that the non-ngram version is already supported in llama.cpp, but turns out it was not the case 😅
|
I know it might be out of scope for implementing support of this model, but could you consider adding the possibility to keep the engrams stored on disk during inference? From what I have read, the engrams are a bit like the Lookup Experts / Lookup Key-Value architecture/approach. For average home system without a lot of CPU-RAM to keep them in memory, reading the engram data from SSD could also be viable? I might be totally wrong, so correct me. |
I haven't read the paper but based on what @ngxson said it uses a "hash-like" operation (ie: https://en.wikipedia.org/wiki/Feature_hashing) which will spread the embeddings throughout the store. If so, then this will effectively end up being a "random access" pattern that won't be easy to prefetch off of disk. Without using the "hash trick" the Cartesian product of the vocab would quickly get out of hand (and also training would be very hard / impossible due to the extreme sparsity), but this would actually be much easier to lay out on disk for prefetching. |
|
I think a normal embedding (not ngram embedding) is also categorized as "random access". If we know which embeddings to be prefetched, we should have known it's correspond to which token, and to know which token, we need to firstly... predict it? |
Sorry, I ninja edited my reply whist you were typing this. It's quite a well known trick used for extremely sparse embeddings and IIRC was first used with "Field-aware" factorization machines. The problem with the Cartesian product is that some of the combinations never appear in training (eg: due to being essentially ransom sequences of tokens here or random interactions for FFM), so there is a good reason for using the "hashing trick" to ensure the embedding space is activated approximately equally often... If instead it used Cartesian product n-grams, we could definitely lay it out on disk (as say an complete m-ary tree) so that only |
The difference here is we do actually know the previous But if you hash the embeddings, we can't really do this as even knowing the previous |
If I calculate it correctly, you mean we are loading the whole embedding tensor of dimension The math that I did: 128000 * 8192 * 4 bits = 4194304000 bits = 524288000 bytes = 500MB
If you know the I think by saying that you're assuming the entropy of |
Let's say we use As soon as we finish generating the 1st token we can narrow this down to (2^17)^2 possible n-gram embeddings (that can be used for the 4th token), then after we generate the 2nd token 2^17 possible embeddings, then after we generate the 3rd token only 1 possible embedding. But whilst the 3rd token is being generated we could (in theory) "prefetch" the 2^17 possible embeddings that can be used for the 4th token into RAM and then as soon as we start to generate the 4th token we could (in theory) avoid the added latency of disk access and get the same token generation speed as if the full set of (2^17)^3 n-gram embeddings were stored in RAM. This isn't a great example as the numbers are all too massive anyway, and as as I linked above; there is a very good reason not to use the Cartesian product anyway! But to compare with hashing:
Let's say we use As soon as we finish generating the 1st token we can't narrow this down and all of the |
|
It's not a great example as the numbers are all way too big using this vocab this size and full hidden state dimensions, but with a smaller vocab like 32k, the use of a projection matrix (to upscale to the hidden dimension), and stored on a HDD instead of SSD, it would potentially be worthwhile doing this sort of prefetching (to answer the other guy's question), but only if you can narrow down the possibilities after each previous token (which I don't think we can here, so all this is irrelevant anyway). |
|
@jukofyork so if I understand correctly, if it is not possible to narrow down the required engrams, the software would need to read the whole (or large part) of the engram data (like in tens of GBs)? But this is just my layman understanding, so probably wrong. Thank you for the attention you put into my question - also @ngxson ! |
Full llama.cpp implementation of the LongCat-Flash-Lite architecture (meituan-longcat/LongCat-Flash-Lite), enabling GGUF conversion and inference for this 68.5B MoE model (3-4.5B activated parameters). Key architecture features implemented: - N-gram embedding: 12 polynomial rolling hash tables that augment the base token embedding (combined as base_embed/13 + 12 hash embeds) - Multi-head Latent Attention (MLA) with KV compression and LoRA scaling (sqrt(2) for Q, sqrt(6) for KV) - Mixture of Experts with 256 real + 128 identity experts (top-k=12), identity experts implemented via residual masking - Double-block layout: 14 HF layers map to 28 llama.cpp blocks, with MoE shortcut connections from even to odd blocks - YaRN RoPE (factor=10, freq_base=5M, mscale_all_dim=1) Achieves ~57 tok/s at Q4_K_M on NVIDIA GB10. Prior art and acknowledgments: - ngxson's llama.cpp PRs ggml-org#19167 (N-gram support) and ggml-org#19182 (LongCat-Flash base), both abandoned due to complexity - kernelpool's (Tarjei Mandt) mlx-lm PR ggml-org#819, merged Jan 2026, used as architectural reference - meituan-longcat for the original model (MIT license)
|
https://huggingface.co/meituan-longcat/LongCat-Flash-Lite-Sparse new model using this architecture got released |
d0e14f7 to
a1a2348
Compare
|
Target support: https://huggingface.co/meituan-longcat/LongCat-Flash-Lite
NOTE: I'm having quite limited time recently, but quite interested by the idea of ngram embedding. Pushing this PR in a rough state mostly for discussions.
The most complex part of the model is to integrate the idea of "ngram cache" inside llama.cpp. That means new tokens can "look back" to see N tokens that were added in the past.
For example: when I given ngram_n = 3 and given 5 input tokens t0, t1, t2, t3, t4. Each token can "see" these ngrams:
This sounds similar to SWA logic but I can't wrap my head around how to reuse the code inside
set_input_kq_mask_impl. CC @ggerganov if you have any ideas.And to store the token ID of each position,
llama_kv_cell_extis extended to store this, quite similar to x/y position for m-rope.The current implementation does return the ngram, what's left to do is to use this info to calculate the hash (quite simple, just some multiplications and modulo):