Benchmarking Qwen 3.6 35B MoE (3B active) on an RTX 3090 Benchmarking Qwen 3.6 35B MoE (3B active) on an RTX 3090 using Unsloth's UD-IQ4_NL_XL quantisation achieved up to 140 tokens per second for generation and over 3,300 tok/s for prompt processing with a CUDA-compiled version of Llama.cpp, though VRAM constraints limited context window to 89,600 tokens. Offloading 10 layers' FFNs to CPU enabled full 262,144-token context at 89 tok/s generation and 1,100 tok/s prompt speed. Benchmarking Qwen 3.6 35B MoE 3B active on an RTX 3090 I mentioned I'd got a second RTX 3090 on a group chat, and a friend said: I know this is not really your thing... but let me know how quickly it runs Qwen 3.6 35bn MoE. With only 24gb of VRAM you’ll need to use a 4-bit quantized version and you won’t get a massive context window. But it should still be pretty cool. He's right that it's not really been my thing -- I've been focusing on my own LLMs recently. I decided to dig in a little, and in particular to play with Llama.cpp https://llama-cpp.com/ , which I haven't used for a while. And then things got a tad out of control, and I wound up doing some relatively detailed benchmarking. The headline results: I downloaded Unsloth's UD-IQ4 NL XL quantisation of the model from Hugging Face https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF . With that, using the default Arch build of Llama.cpp, which uses Vulkan under the hood: - Using the GPU only, I was able to get the model to generate at just over 120 tokens per second, and it was able to process the prompt at just less than 2,800 tok/s. However, having the whole model on the GPU didn't leave that much space for the context window -- it was constrained to about 50,000 tokens, compared to the model's native context length of 262,144. - Offloading the FFNs for the first 12 of the model's 40 layers to the CPU managed to reclaim enough VRAM to be able to get the full context length; however, with that setup, things were -- unsurprisingly -- slower. I got just over 65 tok/s for generation, and 600 tok/s for the prompt. Compiling Llama.cpp myself, in order to get the full CUDA version, helped a lot: - With everything on the GPU, I got 140 tok/s for generation and over 3,300 tok/s for the prompt. That was with a context window of 89,600. So everything was better :- - It was also easier to get to the full context length; that needed just 10 layers' FFNs to be offloaded, and at that point I was getting 89 tok/s for generation, and about 1,100 tok/s for the prompt. That was a pretty impressive improvement. But it also showed that the lack of VRAM on the 3090 really does hurt. The friend who asked me about this is running an Intel Arc B70 Pro, with 32 GiB VRAM. He can of course fit the whole 4-bit quantised model on there without any context window reductions, and he says this about throughput: It starts off at 75-80. but drops into the high 50s as the context window expands That's worse than the CUDA-with-offload results above, though I did limit my testing to a 2,457-token prompt, with 6,144 tokens generated. The RTX 5090 has 32 GiB VRAM and fast Nvidia processing -- I imagine things would be a lot better there. Downside: it costs more than three times what you pay for a 3090. Anyway, in the rest of this post, I'll give more details of the benchmark, including charts showing the performance at different numbers of offloaded layers, and comparisons of the Vulkan vs CUDA versions of Llama.cpp for this model. One caveat: Qwen 3.6 is a multimodal model. That means that it has an extra component to map graphical inputs to tokens that can be consumed by the LLM. I didn't realise this as I ran these tests, but you can actually tell it not to use that component if you're only working with text inputs -- for example, --no-mmproj will do the trick with Llama.cpp. If you're working with text-only stuff, you might want to play with that to see if you can get better results -- it will free up some VRAM and might allow you to get longer context lengths, or better performance. But anyway, as with many of my posts, this is a tidied up version of my lab notes, so you can share my learning journey with me -- but if that sounds pointless, and you've landed here because you want to run this model on your own RTX 3090, you can click this link the-results to jump right to the results. The model Qwen3.6-35B-A3B https://huggingface.co/Qwen/Qwen3.6-35B-A3B is a Mixture of Experts model with 35B total parameters, 3B active. The tensor type is BF16, so it's two bytes per parameter. That's ~70 GiB of space required for the model, just for the weights, with no space for attention matrices and the like. We're going to need a quantised version. I'll dig deep into MoE models at some point, but essentially they work by having multiple different FFN blocks for each Transformers layer. After attention, the context vectors are routed to a subset of those blocks, depending on their content 1. The number of active parameters is how many are actually used when running a single token through. Now, that number, 3B, looked really crazy to me as soon as I saw it. Qwen 3.6, as you can see from the model card linked above, has a vocabulary size of 248,320, and an embedding dimensionality of 2,048. It doesn't use weight-tying, so that means that the embedding layer and the output head come out at a hefty 508,559,360 parameters each. Both the embedding layer and the output head are required for every token, of course, so that means that of those 3B active parameters, more than 1B are used just getting tokens into and out of the model Only 2B active parameters are left to handle all of the attention and the active FFNs. That is an imbalance of the same level as you get with GPT-2 small /2026/07/llm-parameter-counts , and was surprising to see. Well, let's think about VRAM requirements. Naively you might think that you only need to keep the active parameters in VRAM, swapping FFN experts in and out as needed. Unfortunately that doesn't work. Let's say you feed in "The fat cat sat on the", hoping for a completion. The whole sequence is processed in parallel that being the point of using a GPT-style LLM rather than, say, an RNN /2025/10/revisiting-karpathy-unreasonable-effectiveness-rnns . You'll need whichever experts are required for all six tokens in the prompt for the first layer, and likewise for all of the context vectors for the prompt in each of the later layers. So, realistically, for a non-trivial prompt, you can't "swap out" the inactive parameters; an MoE buys you lower usage in terms of processing, but not in terms of memory. 2 That said, as we'll see later, you can at least pull stuff out of VRAM into normal RAM, and get some advantages that way. As my friend said, a 4-bit quant would be needed -- perhaps with that, the whole model, both active and inactive parameters, would fit into 35 / 2 = 17.5 GiB of VRAM, leaving 6.5 GiB for activations, attention matrices, and so on? As it turned out, it needed a bit more, and it's worth taking a look into why. Quantisation I must admit that I'd never really looked into quantisation prior to playing with this, and had naively assumed that you basically just took say a BF16 model, scaled the parameters down so that they were say FP4, tweaked the computations a bit and then ran the result. That was completely wrong I'll have to dig into it further in the future, but my new working mental model is that it's more like lossy compression. The weights are stored in a "compressed" format -- one that is designed so that they can be quickly and cheaply "decompressed" by code running on the target platform. So, speaking very loosely, when running an unquantised model, we might have CUDA code doing something like this: - Load BF16 weights from VRAM - Load BF16 data from another bit of VRAM - Do a matrix multiplication of one by the other. - Store the results into VRAM as BF16 ...a quantised model might operate more like this: - Load quantised weights from VRAM - "Decompress" them to BF16 - Load BF16 data from another bit of VRAM - Do a matrix multiplication of one by the other. - Store the results into VRAM as BF16 Understanding that minimal amount clarified three things for me: - Why people talk about things like "4.1-bit quants". We're talking about the average number of bits per weight in the "compressed" model. - Why there are different quants of about the same size for a given model; for example, when we get on to looking at quantised versions of this model later, you'll see 4-bit ones called UD-IQ4 NL , UD-Q4 K M , UD-IQ4 NL XL , and UD-Q4 K XL , with different sizes in terms of GiB. Each of those is using a different set of trade-offs in the quantisation process, so will perform differently on different tasks. I gather that picking the right one for any given task is a bit of an art form. Hugging Face have a summary of the different types they support here https://huggingface.co/docs/hub/main/en/gguf quantization-types , which has some explanation of the differences, but decoding what they're saying there is beyond what I've learnt at this point. - How GPUs can run quants with bits-per-parameter levels that they don't support for calculations. For example, the RTX 3090 supports 32-bit and two forms of 16-bit float16 and BF16 plus integer operations of various bittednesses . 4-bit formats like FP4 are only supported in more recent cards like the RTX 5090. But, of course, we're not doing any 4-bit computations -- it's all in some format that the GPU can handle natively. 3 fn-3 So, with that minimal level of understanding, it was time to dig in Getting Llama.cpp up and running There are a bunch of different ways that people run LLMs locally, so I needed to work out which one would be likely to give the best results. One thing I noticed when googling around for things like "Qwen3.6-35B-A3B RTX 3090" was that pretty much everyone appeared to be using Llama.cpp https://llama-cpp.com/ . That wasn't something I'd used for a couple of years, and I've repaved my machine multiple times since, so it was time to install it. I run Arch Linux, and there's an OS package for it https://wiki.archlinux.org/title/Llama.cpp , so I installed it using the instructions there -- specifically, the ones to run inference using CUDA: sudo pacman -S llama-cpp ggml cuda Now it was time to try a model. I decided to start off with a small one that would fit onto my GPU without any quantisation, just to work out any bugs. I needed the model to be in GGUF format that being what Llama.cpp uses . GGUF stores tensors and metadata -- the actual architectures of the LLMs Llama.cpp supports are baked in to the tool's source -- but, of course, it supports Qwen3, and so a nice simple non-MoE small model that looked like it would fit onto my GPU was Qwen/Qwen3-4B-GGUF https://huggingface.co/Qwen/Qwen3-4B-GGUF . The help page for that model gave this example command to run it: ./llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8 0 --jinja --color -ngl 99 -fa -sm row \ --temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 --presence-penalty 1.5 \ -c 40960 -n 32768 --no-context-shift That was out-of-date and gave various errors which was fair enough, the model being from late December 2025 -- all of seven months old . The fixes were simple enough: llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8 0 --jinja --color on -ngl 99 -fa on -sm row \ --temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 --presence-penalty 1.5 \ -c 40960 -n 32768 --no-context-shift That is, --color and -fa now needed an on argument. I kicked it off, and got an error: Downloading Qwen3-4B-Q8 0.gguf ───────────────────────────────────── 100% Loading model... \1.54.635.084 E llama model load: error loading model: device Vulkan0 does not support split buffers 1.54.635.088 E llama model load from file impl: failed to load model 1.54.635.110 E common fit params: encountered an error while trying to fit params to free device memory: failed to load model |1.54.741.517 E llama model load: error loading model: device Vulkan0 does not support split buffers 1.54.741.519 E llama model load from file impl: failed to load model 1.54.741.521 E cmn common init : failed to load model '/home/giles/.cache/huggingface/hub/models--Qwen--Qwen3-4B-GGUF/snapshots/bc640142c66e1fdd12af0bd68f40445458f3869b/Qwen3-4B-Q8 0.gguf' 1.54.741.523 E srv load model: failed to load model, '/home/giles/.cache/huggingface/hub/models--Qwen--Qwen3-4B-GGUF/snapshots/bc640142c66e1fdd12af0bd68f40445458f3869b/Qwen3-4B-Q8 0.gguf' llama server exited with code 1 1.54.742.424 E srv llama server: exiting due to model loading error Error: the server exited before becoming ready That seemed strange. The instructions I'd followed on the Arch Wiki page were the ones to install Llama.cpp for CUDA, but it was referring to device Vulkan0 . Vulkan is an open GPU-programming language -- it fits into the stack in essentially the same place as CUDA. As always seems to happen with these things, while the open platform is, well, open, and runs on more platforms than the closed one, it is somewhat slower and buggier. So, why did I have a Vulkan version? Looking at the talk page for the package https://wiki.archlinux.org/title/Talk:Llama.cpp was enlightening: user Gotoro wrote: Hello. The article only provides the llama.cpp-vulkan package for GPU inference, even though on AUR there is a CUDA package available from the same submitter 1 I must add that the CUDA package is "Flagged out-of-date 2025-12-22 " ...and got a reply from SH3NG1UN saying: Hi. The reason CUDA was not added is that Vulkan solution is general, performant enough for most hardware setup, and I personally believe that is what edge AI deployment should look like - if your software stack is capable of gaming, it is capable of AI. They went on to say that they would add CUDA support, but they hit an issue because the AUR version that Gotoro had referred to no longer had an active maintainer. For non-Arch users, the AUR is essentially a repository of non-official, community maintained packages. So, it looked like I had a Vulkan stack installed. Could I install the CUDA one from the AUR instead? Over the last few months, there have been security issues with the AUR, where bad actors have picked up ownership of unmaintained packages and put nasty stuff into them -- exfiltration code to steal things like SSH keys and crypto wallets, for example. As a result, I've been pretty cautious about adding on new AUR dependencies -- and this specific one having been unmaintained for a while scared me. I decided that I would stick with Vulkan for now; if I wanted to try with CUDA, I'd compile it myself from source later on. So, that left the error message: device Vulkan0 does not support split buffers I decided to break down the command like step by step, as running random stuff you copied from the Internet is rarely a good way to understand what's going on. llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8 0 We're running llama-cli and specifying a model from Hugging Face -- that's clearly what -hf means. --jinja Per the --help , "whether to use jinja template engine for chat". --color on "Colorize output to distinguish prompt and user input from generations" -ngl 99 This is "max. number of layers to store in VRAM". For this model, I wanted everything in VRAM, so felt that "all" would be better there. 99 basically means "all", at least for any normal-sized model, but I wanted to be explicit. -fa on This switched on Flash Attention, which seemed reasonable. -sm row This was "how to split the model across multiple GPUs". The error was "does not support split buffers", so this sounded relevant Given that I only have one GPU on my machine, I decided that -sm none would be a better option here. At this point I suspected I had worked out how to solve the problem, but decided to take a look at the other options just out of interest. --temp 0.6 --top-k 20 These were clearly the normal sampling parameters that I was used to from creating my own models. --top-p 0.95 This was an extension to the previous sampling params: in addition to the top-k stuff and after it , limit sampling to the tokens that take up the top 0.95 of the probability distribution. --min-p 0 This was just disabling the min-p feature. min-p discards tokens that are less than X times the probability of the most likely token. Apparently using it is a promising alternative to messing around with top-p and top-k . Rabbit hole alert, let's move on. --presence-penalty 1.5 Per the docs, this applies a "repeat alpha presence penalty". That was interesting I googled around a bit and found this page https://mbrenndoerfer.com/writing/repetition-penalties-language-model-generation , where it became clear that it is a trick to stop models from getting stuck in loops. Small models are quite prone to that, so it certainly sounded useful. Perhaps something to dig into more later, but for now, keeping it seemed perfectly reasonable. -c 40960 That was the "size of the prompt context" according to the help -- that is, the amount of the context window that can be taken up by stuff going into the model before it starts generating. -n 32768 ...and this was the other side of that equation: the maximum number of tokens to predict. --no-context-shift Another interesting one, "whether to use context shift on infinite text generation". Context shift, it appears, means that if you keep on generating, it will just drop stuff off the start of the sequence when it reaches the max context length, so that it can just keep going. Seems clever, though there are obvious risks of dropping important bits of context compacting the context would be safer, and that's what most real-world workflows seem to do . But anyway, we're switching that off, so no harm there. So, most of those parameters seemed sensible, but I wanted to set -sm to none so that it didn't try to split things over multiple GPUs when I only had one, and also set -ngl to all rather than 99 , just for tidiness's sake. That gave this command: llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8 0 --jinja --color on -ngl all -fa on -sm none \ --temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 --presence-penalty 1.5 \ -c 40960 -n 32768 --no-context-shift ...and that worked Loading model... ▄▄ ▄▄ ██ ██ ██ ██ ▀▀█▄ ███▄███▄ ▀▀█▄ ▄████ ████▄ ████▄ ██ ██ ▄█▀██ ██ ██ ██ ▄█▀██ ██ ██ ██ ██ ██ ██ ██ ▀█▄██ ██ ██ ██ ▀█▄██ ██ ▀████ ████▀ ████▀ ██ ██ ▀▀ ▀▀ build : b10068-571d0d540d model : Qwen/Qwen3-4B-GGUF:Q8 0 ftype : Q8 0 modalities : text available commands: /exit or Ctrl+C stop or exit /regen regenerate the last response /clear clear the chat history /read