Tech7 min read

Qwen3-Embedding + Qdrant on CPU: 1.1s/query, 1.5-1.7GB RAM

IkesanContents

Update (2026-09-09): Reran the same measurements on the production machine (Ryzen 7 5800HS), and also compared it against the ModelScope API and measured the interference when running alongside a resident TTS/ASR server → Qwen3-Embedding Local CPU vs API: +35% TTS Latency, API Adds None

Last time I locked down the design for long-term StackChan memory with Qwen and Qdrant.
The RTX 3050 Ti laptop running the voice-chat server has VRAM at 3,945/4,096 MiB, so there’s no room left to add Qwen3-Embedding on the GPU.
I’d already decided to run the 0.6B version on CPU, but hadn’t yet measured “RAM usage and processing time, before worrying about storage size.”

So instead of touching that production machine, I measured the two new costs first on my dev machine (Windows 11, RTX 4060 Laptop 8GB, RAM 32GB): Qwen3-Embedding-0.6B on CPU, and Qdrant’s local mode.

This dev machine and the production machine are different on both CPU and GPU, so these numbers don’t transfer directly.
RAM usage should depend mostly on model size and be fairly machine-independent, so that’s the part I focused on. Processing time depends heavily on this machine’s CPU, so I’m treating it as a reference value only.

Test environment

Here’s the setup.

ItemThis dev machineProduction machine (reference)
OSWindows 11Windows 11
CPU13th Gen Intel Core i7-13700H (14 cores/20 threads)AMD Ryzen 7 5800HS (8 cores)
GPUNVIDIA GeForce RTX 4060 Laptop 8GBRTX 3050 Ti Laptop (VRAM 4096MiB, 3945MiB in constant use)
RAM32GB16GB
Python3.14.0Not stated in earlier posts

Production machine numbers come from Building an STT+LLM+TTS server on an RTX 3050 Ti 4GB and Trying Irodori-TTS Japanese voice cloning on an RTX 3050 Ti Laptop with 4GB VRAM.

Setup

I cut a working venv with python -m venv --system-site-packages .venv and reused the globally installed torch, numpy, and psutil. torch was already the CPU build.

Then I added sentence-transformers and qdrant-client. Versions:

PackageVersion
torch2.9.1+cpu
numpy2.3.3
psutil7.1.3
sentence-transformers6.0.1
transformers5.16.1
qdrant-client1.19.0
huggingface-hub1.30.0
scikit-learn1.9.0
pydantic-core2.46.5
tokenizers0.23.2

That’s the state I measured from.

Running Qwen3-Embedding-0.6B on CPU

The test text was about the same length as the JSON example from the previous post: 137 characters of Japanese combining topic, summary, and entities. With this, I measured RSS (the process’s actual resident memory usage) before and after model load, and embedding time for 1, 10, and 100 items.

RSS climbed more after the first real inference than it did after just loading the model.

PointRSS
Before import19.2 MB
After importing sentence_transformers404.9 MB
After model load529.1 MB
After first inference1386.5 MB
After 100-item batch1695.0 MB

Processing time is only long for the load that includes the first download, which is mostly network wait. Once warmed up, single items and batches took about the same time.

ItemTime
Model load (including first download)193.50s
First inference (including lazy init)1.22s
Warm, 1 item1.114s
Warm, 10-item batch (per item)1.0753s
Warm, 100-item batch (per item)1.1789s
Warm, 1 item (truncate_dim=256)1.1208s

With sentence-transformers’ default settings, batching barely shortened the per-item time on CPU — 1.08 to 1.18 seconds/item across the board. Dropping the dimension from 1024 to 256 with Matryoshka Representation Learning (MRL) didn’t change this either. MRL only truncates the output vector; the model itself does the same amount of work.
The benefit of dropping the dimension is a smaller stored vector (4096 bytes down to 1024 bytes) and less memory/disk on the Qdrant side. It doesn’t make embedding itself faster.

I first tried passing truncate_dim=256 as a constructor argument to SentenceTransformer. That built a separate instance alongside the existing 1024-dim model, so RSS from both instances added up. Switching to passing it at call time with model.encode(texts, truncate_dim=256) kept it to a single instance.

Running Qdrant in local mode

QdrantClient(path=...) gives a local mode that persists to disk without running a server. I generated dummy data matching the searchable-memory fields from the previous post, embedded it with Qwen3-Embedding, and inserted 100 records.

My first script used QdrantClient.search(), but qdrant-client 1.19.0 has removed search(), so it raised AttributeError. I switched to query_points() and pulled results from resp.points instead, then reran it.

ItemValue
Vectorizing 100 records0.5997s/record
Upsert of 100 records0.429s
Similarity search only (top 5)0.00698s
Search with entities filter (top 5)0.00370s
Disk usage of qdrant_data directory0.99 MB
RSS with both running (after upsert)1562.1 MB

The filtered search on entities was faster than the unfiltered one — at 100 records, the reduced search space seems to outweigh the filtering overhead. Combined RSS for the embedding model and Qdrant together comes out to roughly 1.5-1.7GB on this dev machine.

Searching for “what did I run on the RTX 3050 Ti laptop” pulled the only dummy record mentioning the RTX 3050 Ti — the “StackChan voice backend” topic — into the top 3 results.

RankScore
1st0.4762
2nd0.4692
3rd0.4667

Getting the original turn back out of SQLite

Up to this point I’d only measured embedding and Qdrant on their own — I hadn’t tried the design from the previous post, where source turns stay in SQLite and Qdrant holds only the summary vectors and search fields. So I built the SQLite schema for real and checked whether a search result could actually get back to the original turn in SQLite.

Two tables: turns and memories.

TableColumns
turnsturn_id, speaker, text, occurred_at
memoriesmemory_id, topic, summary, entities, memory_type, occurred_at, source_turn_ids, status, qdrant_point_id

memories.qdrant_point_id links to the matching point on the Qdrant side.

Five topics went in: the voice backend, buying a PC, wiring up the CO2 monitor’s sensor, a favorite drink, and a chat about Qdrant filters — each repeated 4 times for 20 topic instances. Each topic carries one user turn and one assistant turn, so that’s 40 turns and 20 memories. Summaries came from a fixed template rather than Qwen, standing in for the real thing.

ItemValue
SQLite write (40 turns + 20 memories)0.0030s
Vectorizing 20 records13.560s (0.678s/record)
Qdrant upsert (20 records)0.0954s
Search (including embedding the query)0.2673s
Restoring source turns from SQLite (via source_turn_ids)0.00054s
SQLite file size24.0 KB
Qdrant disk usage0.21 MB

Searching “what did I end up doing about the Fujitsu PC thing” hit the “buying a PC” topic at the top (score 0.6801). Its point ID led through the memories table to the turns table, pulling back the two original turns.

turn12 [user] PCどれ買った?
turn13 [assistant] 富士通のesprimoにした。安定重視で選んだ。

The flow from the previous post’s design worked as intended: search the summaries in Qdrant, follow memory_id back to the original turn in SQLite. Restoring from SQLite was sub-millisecond and clearly not a bottleneck — most of the time went to embedding the query text (0.267s).

Does this fit on the production machine?

Matching this dev machine’s measurements (roughly 1.5-1.7GB combined for the embedding model and Qdrant) against the production machine’s free RAM: my notes from when I was testing STT and TTS there put the free RAM at about 6.5GB out of 16GB. Whatever this dev machine uses would fit inside that.

On the time side, the design only calls the embedding once per turn when a search is actually needed, so it’s not a cost that hits every turn. On the turns where it does run, it still adds about a second of latency just for the embedding. The production CPU is a Ryzen 7 5800HS, a different generation from this dev machine’s i7-13700H.

Taking this code to the production machine (the laptop running Irodori-TTS), here’s what I’ll check:

  • Whether the current free RAM still matches the ~6.5GB on record
  • Actual RAM increase and processing time running Qwen3-Embedding-0.6B on the Ryzen 7 5800HS
  • Qdrant local mode insert/search speed
  • Whether running it alongside STT/TTS causes OOM or a slowdown