Technical aspects of running local LLMs on FreeBSD

cracauer@

Developer
This is a continuation on my local LLM thread, which is hard to reply to. This one might be easier.



I wanted to waffle a bit more about the hardware involved. You basically have two approaches:
  • Get a fat GPU with a certain amount of VRAM. You will be able to run LLMs up to the VRAM size fast. Overflowing models up to RAM size of the machine will run slow but not necessarily catastrophic if you still have a good chunk in VRAM.
  • Get a machine with integrated GPU that shares RAM with the GPU. This gets you much more memory for the GPU than you can afford with a dedicated GPU. It will run models up to RAM size at mediocre to OK speed.

Cost-wise it is a wash. For $3300-$3500 you can pick between:
  • A used NVidia 5090 with 32 GB VRAM
  • 3x NVidia 3090 and a really fat power supply
  • An AMD Strix Halo (Ryzen AI 395) with 128 GB RAM (shared GPU memory)
  • An Apple Mac with a M4 Max and 128 GB RAM (shared GPU memory)

The Apple Mac doesn't run all models well. Some models are marked as optimized for Metal (Apple's equivalent to Vulkan).

I have been told that Strix Halo prefers to run Mixture of Expert models and doesn't do so well on dense models. I don't have one of those suckers so I can't comment. There's also the aspect of getting FreeBSD to first run on the thing (we have recently seen a failure) and getting Vulkan up.

I have the single NVidia card, which runs out of the box for FreeBSD. But alas everything bigger than 32 GB is slow. There are many interesting models between 32 and 128 GB. But on the other hand, the models that do fit are very fast. You can also use the performance advantage to increase the context size (although that costs further VRAM). I also plan to do extensive experiments with post-training and agents. Speed is more important in that case than when just running a chat through the web browser.

I do not know whether multi-GPU works for one of the llamas on FreeBSD. On paper 3x 3090 looks really attractive since it gives you 72 GB of VRAM for the same price. And functions as heating in winter. Just for starters it might be that multi-GPU in llama.cpp only works with CUDA, not Vulkan.

Theoretically the NVidia GPUs have another advantage: you can run all that GPU software that only has CUDA backends. At the time of this writing CUDA does not work on FreeBSD through Linuxulator, though.

Finally just a word on commercial LLMs: cost-wise it is clearly best to just use up as many of the $20/month plans. They are heavily sponsored. Buying your own hardware can't compete price-wise. But Anthropic might or might not have kicked Claude Code out of that plan:
 
First attempt on my Raptor Lake with UHD730 graphics that should work:
Code:
ggml_vulkan: Found 1 Vulkan devices:
ggml_vulkan: 0 = Intel(R) Graphics (RPL-S) (Intel open-source Mesa driver) | uma: 1 | fp16: 1 | bf16: 0 | warp size: 32 | shared memory: 65536 | int dot: 1 | matrix cores: none
Illegal instruction (core dumped)
Do I need a proprietary Intel driver? Never seen it...
Next attempt can be a Ryzen 7 with GTX1050.
 
First attempt on my Raptor Lake with UHD730 graphics that should work:
Code:
ggml_vulkan: Found 1 Vulkan devices:
ggml_vulkan: 0 = Intel(R) Graphics (RPL-S) (Intel open-source Mesa driver) | uma: 1 | fp16: 1 | bf16: 0 | warp size: 32 | shared memory: 65536 | int dot: 1 | matrix cores: none
Illegal instruction (core dumped)
Do I need a proprietary Intel driver?

Not sure. What does `vulkaninfo --summary` from port vulkan-tools report? Maybe you only have the Mesa software renderer?
 
Not sure. What does `vulkaninfo --summary` from port vulkan-tools report? Maybe you only have the Mesa software renderer?
Code:
==========
VULKANINFO
==========

Vulkan Instance Version: 1.4.336


Instance Extensions: count = 24
-------------------------------
VK_EXT_acquire_drm_display             : extension revision 1
VK_EXT_acquire_xlib_display            : extension revision 1
VK_EXT_debug_report                    : extension revision 10
VK_EXT_debug_utils                     : extension revision 2
VK_EXT_direct_mode_display             : extension revision 1
VK_EXT_display_surface_counter         : extension revision 1
VK_EXT_headless_surface                : extension revision 1
VK_EXT_surface_maintenance1            : extension revision 1
VK_EXT_swapchain_colorspace            : extension revision 4
VK_KHR_device_group_creation           : extension revision 1
VK_KHR_display                         : extension revision 23
VK_KHR_external_fence_capabilities     : extension revision 1
VK_KHR_external_memory_capabilities    : extension revision 1
VK_KHR_external_semaphore_capabilities : extension revision 1
VK_KHR_get_display_properties2         : extension revision 1
VK_KHR_get_physical_device_properties2 : extension revision 2
VK_KHR_get_surface_capabilities2       : extension revision 1
VK_KHR_portability_enumeration         : extension revision 1
VK_KHR_surface                         : extension revision 25
VK_KHR_surface_protected_capabilities  : extension revision 1
VK_KHR_wayland_surface                 : extension revision 6
VK_KHR_xcb_surface                     : extension revision 6
VK_KHR_xlib_surface                    : extension revision 6
VK_LUNARG_direct_driver_loading        : extension revision 1

Instance Layers:
----------------

Devices:
========
GPU0:
    apiVersion         = 1.3.278
    driverVersion      = 24.1.7
    vendorID           = 0x8086
    deviceID           = 0xa780
    deviceType         = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
    deviceName         = Intel(R) Graphics (RPL-S)
    driverID           = DRIVER_ID_INTEL_OPEN_SOURCE_MESA
    driverName         = Intel open-source Mesa driver
    driverInfo         = Mesa 24.1.7
    conformanceVersion = 1.3.6.0
    deviceUUID         = 868080a7-0400-0000-0002-000000000000
    driverUUID         = f303ef53-4163-bd95-7437-925e093696ce
GPU1:
    apiVersion         = 1.3.278
    driverVersion      = 0.0.1
    vendorID           = 0x10005
    deviceID           = 0x0000
    deviceType         = PHYSICAL_DEVICE_TYPE_CPU
    deviceName         = llvmpipe (LLVM 15.0.7, 256 bits)
    driverID           = DRIVER_ID_MESA_LLVMPIPE
    driverName         = llvmpipe
    driverInfo         = Mesa 24.1.7 (LLVM 15.0.7)
    conformanceVersion = 1.3.1.1
    deviceUUID         = 6d657361-3234-2e31-2e37-000000000000
    driverUUID         = 6c6c766d-7069-7065-5555-494400000000
 
You have the hardware renderer.

Did you install llama and ggml from packages? Maybe this is an issue of being compiled in a context where native CPU was detected during compilation? Although Raptor Lake should not miss much except avx512.
 
You have the hardware renderer.

Did you install llama and ggml from packages? Maybe this is an issue of being compiled in a context where native CPU was detected during compilation? Although Raptor Lake should not miss much except avx512.
That's possible. I'm not sure about the origin of everything.
It's also not a GENERIC 15 kernel. Some things are changed.I don't think it can be related but it may be a problem. System was still copying so I didn't want to reboot. I'll try a clean kernel and world in a few hours.
 
Qwen 3.6 is out as a dense model (that means that all parameters are active, as oppose to a Mixture of Experts model).

Here is how to find the file to download. Visit the models homepage on Huggingface:

Can't hurt to read some of the drivel on there about the care and feeding of this particular model.

Click on the tab "files and versions":

Pick a quantization and size that fits your computer. Copy the link to the gguf file.

fetch https://huggingface.co/unsloth/Qwen3.6-27B-GGUF/resolve/main/Qwen3.6-27B-Q6_K.gguf

Then start llama.cpp with --model <thatfileondisk>.

There is also an actual homepage for the model:

This particular homepage is not very good. It doesn't have recommendations for parameters like temperature, kv cache and so on.
 
Heads up: version 8895 of llama.cpp has been committed to ports.

This changes the location where llama.cpp stores models automatically downloaded from huggingface (as in post #1 here).
Old cache: ~/.cache/llama.cpp/
New cache: ~/.cache/huggingface/hub

It will automatically move already downloaded models to the new location. If you have different filesystems under there you might be in for a surprise.

In my case the homedir is on NFS and there is a symlink for the old cache to a local NVMe SSD. So it moved things to NFS and afterwards I made to move them back via a new symlink to the SSD. Took 3 hours I'm never getting back.
 
Did you install llama and ggml from packages? Maybe this is an issue of being compiled in a context where native CPU was detected during compilation?
GGML has the ability to detect your CPU at runtime and fallback from whatever it doesn't support. I've patched the ports file to make use of it here:


Note the downgrade of llvm from 22 to 21 in order to workaround a llvm22 regression, which is why I haven't PR-ed it.

Although Raptor Lake should not miss much except avx512.
On current processors, the performance degradation of scheduling avx512 code on an efficiency core instead of a performance core is significantly larger then the degradation of scheduling avx2 code on an e-core instead of a p-core, or in other words, the relative performance to p-cores of e-cores is higher when executing avx2 then it is when execuring avx512. Since FreeBSD-15.0 has no awareness of the cpu design, it will run the inference software on whatever cores are least loaded at the time the inference software is launched, and then will keep it sticked on that cores unless other cores have at least kern.sched.steal_tresh less load on them, which means odds are high that llama.cpp starts on an e-core, allocates mostly e-cores and then stays there. The CPU firmware will then identify llama to be avx heavier then most other workloads and generate an advisory asking the OS to reschedule it onto a p-core, however FreeBSD-15.0 will ignore that advisory and keep it on the e-core(s) nevertheless. So until FreeBSD learns how to address scheduler advisories (currently tracked under D54674 for scheduler advisories in general and D44456 for intel specific Intel Thread Director register processing) there's no point in using avx512 even if your CPU does support it.
 
Great topic for a thread, thanks cracauer@.

Here's some technical aspects for running AI models locally on unbalanced crap hardware with llama-cpp, some random AI models from huggingface and aider.

The crap Hardware:

CPU Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz ~ 2011
16 Gb DD3 RAM @ 1600 ~ 2007
RTX 3060 with 12 Gb NVRAM ~ 2021

Installations:

llama-cpp
git clone https://github.com/ggml-org/llama.cpp[/INDENT][/INDENT][/INDENT][/INDENT][/INDENT]
[INDENT=2][INDENT=2][INDENT=2][INDENT=2][INDENT=2]cd llama.cpp[/INDENT][/INDENT][/INDENT][/INDENT][/INDENT]
[INDENT=2][INDENT=2][INDENT=2][INDENT=2][INDENT=2]mkdir build[/INDENT][/INDENT][/INDENT][/INDENT][/INDENT]
[INDENT=2][INDENT=2][INDENT=2][INDENT=2][INDENT=2]cd build[/INDENT][/INDENT][/INDENT][/INDENT][/INDENT]
[INDENT=2][INDENT=2][INDENT=2][INDENT=2][INDENT=2]cmake -DGGML_VULKAN=1 ..[/INDENT][/INDENT][/INDENT][/INDENT][/INDENT]
[INDENT=2][INDENT=2][INDENT=2][INDENT=2][INDENT=2]cmake --build . --config Release -j 3[/INDENT][/INDENT][/INDENT][/INDENT][/INDENT]
[INDENT=2][INDENT=2][INDENT=2][INDENT=2][INDENT=2]cp llama* lib*.so* ~/.local/bin/

aider - this one is a bit trickier - git clone the source and then pip install in a venv and fix the dependency errors - it can be done but requires some kerfuffling​

Task for AI to do - the prompt, the testcase, whatever you want to call it. The goal is to make AI write the code and test until all errors are gone without intervention.

Create a new C file with a descriptive name. Inside it, implement a function to read the value for the CPU temperature using sysctlbyname with the modern path "dev.cpu.0.temperature". Note that FreeBSD returns this value as an integer in deci-Kelvin (e.g., 3000 means 300.0 Kelvin). Convert this value to Celsius by dividing by 10.0 and subtracting 273.15, then print it out in main() Also in the same code read the values for load average and print those out. If the compilation fails, intercept the errors and fix them.

The random models and how they did.

1)4.36Gqwen2.5-coder-7b-instruct-q4_k_m.ggufsuccess for load average but fail for temperature~ 54 t/s
2)6.87Ggemma-4-12b-Q4_K_M.gguffails caught in a reasoning loop~ 29 t/s
3)6.87Ggemma4-coding-Q4_K_M.ggufsuccess~ 30 t/s
4)7.95GLlama-3-Hercules-5.1-8B-Q8_0.gguffails to create the file~ 35 t/s
5)8.01GCodestral-22B-v0.1-IQ3_XXS.ggufsuccess~ 18 t/s
6)8.37Gqwen2.5-coder-14b-instruct-q4_k_m.ggufsuccess for temperature fail for load averages~ 30 t/s
7)8.43Gmicrosoft_Phi-4-reasoning-Q4_K_M.gguffails by hanging when fixing error~ 32 t/s
8)8.65GDevstral-Small-2-24B-Instruct-2512-UD-Q2_K_XL.ggufsuccess for temperature fails for load average~ 19 t/s
9)8.74GNorth-Mini-Code-1.0-UD-IQ1_M.ggufsuccess~ 54 t/s
a)8.91GQwen3.6-27B-UD-IQ2_XXS.gguffails~ 18 t/s
b)9.11Ggemma4-coding-Q6_K.ggufsuccess for temperature fails for load averages~ 25 t/s
c)9.11GQwen3.5-9B-Q8_0.ggufsuccess for temperature fails for load averages~ 25 t/s
d)14.84GQwen2.5-Coder-32B-Instruct-Q3_K_M.ggufsuccess for temperature fails slightly for load averages (2/3)~ 1 t/s

Warning:
If you run the below scripts you are responsible. You will see llama-cpp warnings like below so make sure you are safe.
srv llama_server: -----------------
srv llama_server: CORS is set to allow all origins ('*') and no API key is set
srv llama_server: this can be a security risk (cross-origin attacks)
srv llama_server: more info: https://github.com/ggml-org/llama.cpp/pull/25655
srv llama_server: -----------------
srv llama_server: -----------------
srv llama_server: the following feature(s) are enabled:
srv llama_server: server tools (experimental)
srv llama_server: do not expose the server to untrusted environments
srv llama_server: -----------------


Comments:
- llama-cpp was installed from ports first, that might have helped to build it from source
- for aider the dependencies that caused errors were installed as pkg and then edited out from requirements.txt
- the reason speed drops for models sized > NVRAM is that the CPU/RAM slows everything down when offloading from NVRAM is being done
- there is margin in the memory management in llama startup script to allow kde and chromium to run at the same time as the ai model
- there is a feeling that Vulkan not always is giving memory back after stopping llama-server with ctrl-c

Edit:
The script below has been tweaked and updated.

This script will run models up to ~19GB in size for any crap old hardware with 16GB RAM and 12GB NVRAM which means 32/34B models with Q4_K_M/Q3_K_M. The speed is estimated to around 0.5-1.5 t/s for any model that goes above 12GB. For smaller models speed is estimated to around 20-50 t/s.

Script to start llama and pick a model:

sh:
#!/bin/sh

MODELPATH="$HOME/AI-models"
TOTAL_VRAM=12.0      # Physical VRAM in GB
MAX_LOCKED_RAM=10.0  # FreeBSD limit for mlock in GB

# Path to your working Python 3.12 binary inside your venv
PY_BIN="$HOME/venvs/aider-env/bin/python3"

run_llama() {
    FILE_NAME="$1"
    FILE_SIZE_GB="$2"
    FULL_PATH="$MODELPATH/$FILE_NAME"

    echo "──────────────────────────────────────────────────────"
    echo "📊 Analyzing $FILE_NAME ($FILE_SIZE_GB GB)..."

    # Verify NVIDIA GPU and driver status cleanly
    echo "🎮 Verifying NVIDIA GPU status..."
    if [ ! -c /dev/nvidia0 ]; then
        echo "❌ Error: NVIDIA device node (/dev/nvidia0) is missing!"
        return 1
    fi

    NV_VERSION=$(sysctl -n hw.nvidia.version 2>/dev/null)
    if [ -z "$NV_VERSION" ]; then
        echo "❌ Error: NVIDIA kernel module is not active!"
        return 1
    fi
    echo "✅ GPU Status: NVIDIA Graphics Driver ($NV_VERSION) is active."

    echo "🧠 Reading model built-in metadata live..."

    export TARGET_GGUF_PATH="$FULL_PATH"

    METADATA=$("$PY_BIN" -c '
import sys, struct, os

def read_gguf_metadata(filepath):
    ctx_val, blocks_val = None, None
    try:
        with open(filepath, "rb") as f:
            # Verify Magic Header "GGUF"
            magic = f.read(4)
            if magic != b"GGUF": return None, None
          
            # Read Version (uint32) and V2/V3 fields
            version = struct.unpack("<I", f.read(4))[0]
            if version not in [2, 3]: return None, None
          
            # Read counts
            tensor_count = struct.unpack("<Q" if version == 3 else "<I", f.read(8 if version == 3 else 4))[0]
            kv_count = struct.unpack("<Q" if version == 3 else "<I", f.read(8 if version == 3 else 4))[0]
          
            # Types map according to GGUF spec
            # 0=uint8, 1=int8, 2=uint16, 3=int16, 4=uint32, 5=int32, 6=float32, 7=bool, 8=string, 9=array...
            for _ in range(kv_count):
                # Read key string
                key_len = struct.unpack("<Q" if version == 3 else "<I", f.read(8 if version == 3 else 4))[0]
                key = f.read(key_len).decode("utf-8", errors="ignore")
              
                # Read value type
                val_type = struct.unpack("<I", f.read(4))[0]
              
                # Helper to skip or read values based on type
                def skip_value(t):
                    if t in [0, 1, 7]: f.seek(1, 1)
                    elif t in [2, 3]: f.seek(2, 1)
                    elif t in [4, 5, 6]: return struct.unpack("<I", f.read(4))[0]
                    elif t == 8: # String
                        s_len = struct.unpack("<Q" if version == 3 else "<I", f.read(8 if version == 3 else 4))[0]
                        f.seek(s_len, 1)
                    elif t == 9: # Array
                        arr_type = struct.unpack("<I", f.read(4))[0]
                        arr_len = struct.unpack("<Q" if version == 3 else "<I", f.read(8 if version == 3 else 4))[0]
                        for _ in range(arr_len): skip_value(arr_type)
                    elif t in [10, 11, 12, 13]: f.seek(8, 1) # uint64, int64, float64
                    return None

                res = skip_value(val_type)
              
                # Capture our specific targets cleanly
                if "context_length" in key and res:
                    ctx_val = res
                elif "block_count" in key and res:
                    blocks_val = res
                  
                if ctx_val and blocks_val:
                    break
    except Exception:
        pass
    return ctx_val, blocks_val

ctx, blocks = read_gguf_metadata(os.environ.get("TARGET_GGUF_PATH", ""))
ctx_str = str(ctx) if ctx else ""
blocks_str = str(blocks) if blocks else ""
print(ctx_str + " " + blocks_str)
')

    TRAINED_CTX=$(echo "$METADATA" | awk '{print $1}')
    ACTUAL_LAYERS=$(echo "$METADATA" | awk '{print $2}')

    if [ -z "$TRAINED_CTX" ] || [ "$TRAINED_CTX" -lt 2048 ]; then
        TRAINED_CTX=32768
        echo "ℹ  Could not read trained context length. Using safe fallback: $TRAINED_CTX"
    else
        echo "🧠 Model is natively trained for a max of: $TRAINED_CTX tokens."
    fi

    if [ -z "$ACTUAL_LAYERS" ] || [ "$ACTUAL_LAYERS" -lt 10 ]; then
        ACTUAL_LAYERS=64
        echo "ℹ  Could not read layer count. Using baseline fallback: $ACTUAL_LAYERS layers."
    else
        echo "🧱 Model has an architectural structure of: $ACTUAL_LAYERS layers."
    fi

#
# MEMORY MANAGEMENT (DYNAMIC VRAM/RAM SPLIT & DYNAMIC KV CACHE)
#
    EVAL_OPTS=$(awk -v vram="$TOTAL_VRAM" -v ram="$MAX_LOCKED_RAM" -v size="$FILE_SIZE_GB" -v max_ctx="$TRAINED_CTX" -v total_layers="$ACTUAL_LAYERS" '
    BEGIN {
        system_vram_overhead = 1.8;
        available_vram = vram - system_vram_overhead;
      
        # SCENARIO 1: Model fits entirely in VRAM -> Use full quality f16 KV Cache
        if (size <= available_vram) {
            cache_type = "f16";
            cache_factor = 0.0065; # Full precision footprint
          
            vram_left_for_cache = available_vram - size;
            calculated_ctx = int((vram_left_for_cache * 1000) / (total_layers * cache_factor));
            ctx = int(calculated_ctx / 1024) * 1024;
          
            if (ctx < 2048) ctx = 2048;
            if (ctx > max_ctx) ctx = max_ctx;
          
            ngl = total_layers + 5;
            ram_fallback = 0;
            b = 512; ub = 256;
            mode = "mlock"; # Safe to lock in RAM since 0 model layers spill over
        }
        # SCENARIO 2: Model requires split-mode -> Switch to q4_0 KV Cache to save VRAM for layers
        else {
            cache_type = "q4_0";
            cache_factor = 0.0017; # Compressed footprint
          
            ctx = 4096;
            if (ctx > max_ctx) ctx = max_ctx;
          
            cache_vram_cost = (ctx / 1000) * total_layers * cache_factor;
            usable_vram_for_layers = available_vram - cache_vram_cost;
          
            if (usable_vram_for_layers < 0) usable_vram_for_layers = 0;
          
            vram_ratio = usable_vram_for_layers / size;
            ngl = int(total_layers * vram_ratio);
          
            if (ngl < 0) ngl = 0;
            if (ngl > (total_layers + 5)) ngl = total_layers + 5;
          
            ram_fallback = size - usable_vram_for_layers;
            b = 512; ub = 256;
            mode = "mmap"; # Use standard mmap to safely spill past FreeBSD mlock limits
        }
      
        if (ram_fallback > ram) {
            print "ERROR: Model requires " ram_fallback " GB system RAM offload, which exceeds your 10 GB limit!" > "/dev/stderr";
            printf "0 ERROR 0 0 0 error\n";
            exit 1;
        }
      
        printf "%d %s %d %d %d %s\n", ctx, cache_type, b, ub, ngl, mode;
    }')

    read -r ctx type b ub ngl mode <<EOF
$EVAL_OPTS
EOF

    if [ "$type" = "ERROR" ] || [ -z "$ctx" ] || [ "$ctx" -eq 0 ]; then
        echo "❌ ERROR: Launch Aborted! Constraints not met."
        return 1
    fi

    # Calculate safe prompt margin
    SAFE_PROMPT_LIMIT=$((ctx - 512))
    if [ "$SAFE_PROMPT_LIMIT" -lt 1024 ]; then
        SAFE_PROMPT_LIMIT=$ctx
    fi

    echo "⚙️  Optimized profile: CTX=$ctx, NGL=$ngl, BATCH=$b, UBATCH=$ub, CACHE=$type, MODE=$mode"
    echo "🔄 Context Shifting: Active (Infinite scrolling chat enabled)"
    echo "💡 Safe Prompt Limit: Do not exceed $SAFE_PROMPT_LIMIT tokens in a SINGLE prompt."
    echo "──────────────────────────────────────────────────────"

    # Start the server with q4_0 KV cache flags and flash attention
    llama-server \
        -t 3 \
        -b "$b" \
        -tb 4 \
        -ub "$ub" \
        -c "$ctx" \
        --context-shift \
        -fa on \
        --load-mode "$mode" \
        -ctk "$type" \
        -ctv "$type" \
        --jinja \
        --host 0.0.0.0 \
        --port 8080 \
        --alias kAI \
        --temp 0.4 \
        --top-p 0.1 \
        --min-p 0.05 \
        --tools all \
        -m "$FULL_PATH"
}

# Check if model directory exists
if [ ! -d "$MODELPATH" ]; then
    echo "❌ Error: Model directory $MODELPATH does not exist."
    exit 1
fi

echo "Pick an AI model to run: "

index=1
chars="123456789abcdefghijklmnopqrstuvwxyz"

MATCHED_MODELS=$(find "$MODELPATH" -maxdepth 1 -name "*.gguf" 2>/dev/null | while read -r path; do
    bytes=$(stat -f %z "$path")
    name=$(basename "$path")
    echo "$bytes $name"
done | sort -n)

old_ifs=$IFS
IFS='
'
for line in $MATCHED_MODELS; do
    IFS=$old_ifs
    bytes=$(echo "$line" | awk '{print $1}')
    name=$(echo "$line" | cut -d' ' -f2-)
    size_gb=$(awk -v b="$bytes" 'BEGIN {printf "%.2f", b / 1024 / 1024 / 1024}')
    selector=$(echo "$chars" | cut -c "$index")
    if [ -z "$selector" ]; then
        break
    fi
    eval "MENU_KEY_$selector=\"$name\""
    eval "MENU_SIZE_$selector=\"$size_gb\""
    printf "%s)   %6sG   %s\n" "$selector" "$size_gb" "$name"
    index=$((index + 1))
    IFS='
'
done
IFS=$old_ifs
echo "0) Exit"

printf "Your choice: "
read choice

if [ "$choice" = "0" ] || [ -z "$choice" ]; then
    echo "Exiting."
    exit 0
fi

eval "SELECTED_MODEL=\$MENU_KEY_$choice"
eval "SELECTED_SIZE=\$MENU_SIZE_$choice"

run_llama "$SELECTED_MODEL" "$SELECTED_SIZE"

Script to start aider:

sh:
#!/bin/sh

#export HF_HUB_DISABLE_XET=1

# Export env flags
export AIDER_MODEL="openai/kAI"
export AIDER_OPENAI_API_BASE="http://127.0.0.1:8080"
export OPENAI_API_BASE="http://127.0.0.1:8080"
export OPENAI_API_KEY="local-no-key-needed"
export AIDER_SYSTEM_PROMPT="
CRITICAL REQUIREMENT: Always write all of your C code, scripts,
configurations, and experiments into a single file named exactly
"new.c". Never invent other filenames.
You are an expert software developer specializing in FreeBSD.
Always ensure configurations, scripts, and code adhere strictly
to modern FreeBSD best practices and style guides (ZFS, Jails,
bhyve, pkg, and ports). Do not mix Linux commands or paths into
FreeBSD code. Do not mix code for OpenBSD into FreeBSD code.
Always assume the target environment compiles code exclusively with Clang/LLVM.
Ensure all configurations, scripts, and code adhere strictly to modern
FreeBSD best practices. Do not mix Linux commands or GCC-specific flags into code."

#export AIDER_NO_REPO_MAP="true"
#export TREE_SITTER_LANGUAGE_PACK_CACHE="/tmp/empty-syntax-cache"

# Check that AI-model is there
if ! nc -z -w 2 127.0.0.1 8080 > /dev/null 2>&1; then
    echo "❌ Error: llama.cpp is not running on port 8080!"
    exit 1
fi

# Activate the venv
. "$HOME/venvs/aider-env/bin/activate"
export PYTHONPATH="$HOME/aider"
rm -f .env

# Startup aider with the correct map disable flag
"$HOME/venvs/aider-env/bin/aider" \
 --lint-cmd "c: clang -fsyntax-only" \
 --test-cmd "$HOME/scripts/test-runner.sh" \
 --map-tokens 0 \
 --edit-format diff \
 --no-show-model-warnings \
 --yes-always

The test-runner.sh script that will run when /test is run in aider:

sh:
#!/bin/sh
# test_runner.sh

# 1. Automatically grab ALL .c files present in the current directory
ALL_FILES=$(ls *.c 2>/dev/null)

if [ -n "$ALL_FILES" ]; then
    # 2. Pick a name for the binary based on the most recently modified C file
    MAIN_FILE=$(git diff-tree --no-commit-id --name-only -r HEAD | grep "\.c$" | head -n 1)
    if [ -z "$MAIN_FILE" ]; then
        MAIN_FILE=$(ls -t *.c | head -n 1)
    fi
    BINARY="${MAIN_FILE%.c}"

    echo "⚙ Automatically compiling all files ($ALL_FILES) into binary [$BINARY] using clang..."

    # 3. Compile every single C file together
    clang $ALL_FILES -o "$BINARY" && ./"$BINARY"
else
    echo "❌ Error: Aider test runner couldn't locate any active C files."
    exit 1
fi

/grandpa
 
Last edited:
At the time of this writing CUDA does not work on FreeBSD through Linuxulator, though.

You can run Ollama with Cuda on Freebsd using a Rocky Linux Podman container

The Nvidia Driver and Linuxulator libraries are mounted from Freebsd into the Podman container,
and you can start and stop Ollama with a Makefile by running make to start Ollama and make clean to stop it




Code:
[i] Yes Master ? doas podman exec -it -u ${USER} ollama /bin/bash
bash-5.1$ nvidia-smi
Sun Sep  6 15:16:25 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 595.84                 Driver Version: 595.84         CUDA Version: 13.2     |
+-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce GTX 1650        Off |   00000000:01:00.0 Off |                  N/A |
| N/A   72C    P0             16W /   50W |    2547MiB /   4096MiB |     11%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI              PID   Type   Process name                        GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
|    0   N/A  N/A           26578      C   /usr/lib/ollama/llama-server           2546MiB |
+-----------------------------------------------------------------------------------------+
 
You can run Ollama with Cuda on Freebsd using a Rocky Linux Podman container

Hey NapoleonWils0n - I have had your threads about podman projects on my screen for a week now.

I just need to find a little time and then I will look at the cuda uvm hack and see if I can puzzle it together with llama-cpp. If I understand it correctly llama-cpp is the engine in Ollama but I'm new to all this ai kerfuffling.

I think I saw that you have a thread about finding an x11 guinea pig and I will reply to that since my build-from-ports nvidia drivers forces me to use x11 on this old unbalanced hardware.

/grandpa
 
HI Mate

The Rocky Linux Podman container downloads the Ollama binaries using the Dockerfile

The project lets you create a custom directory to store your own projects that wont be tracked by git

So you can just duplicate the olllama project
and change the Dockerfile to download llama-cpp if you want

Also all the Podman container use networking host
which means they are accessible on 127.0.0.1 from the freebsd side

There are 2 base images you build either X11 or Wayland
then you build the containers like Ollama ontop of those

As Ollama isnt a gui app it wont make much difference
 
I know not acknowledging previous work is some people's favorite pastime, but this is getting truly ridiculous.

I did say that the project depended on your work with the dummy-uvm.so
in the videos, documentation and notes to get Nvidia working.

Without the dummy-uvm.so using Jails and Podman with Cuda wouldnt be possible.

The post was just a brief one line description of how the project worked.

North_Promise_9835 on reddit updated the uvm_ioctl_override.c
about 5 months ago because the dummy-uvm.so stopped working after the 535 driver


North_Promise_9835 fixes for the uvm_ioctl_override.c


Cuda works in both Jails and Rocky Linux Podman containers
im not a C programmer but everything works perfectly with the 595.84 driver.

I mention the the reddit post here about the fixes.


In all the other videos about the project i always make sure to mention your work
with the uvm_ioctl_override.c and dummy-uvm.so

So i dont want you to think im talking credit or not acknowledging your work.

Everything else in the project are my own ideas and work,
mounting the Nvidia driver and libraries from the Linuxulator into the Podman container.

Instead of manually installing the Nvidia driver in the Jail or Podman container
that matches the version on the Freebsd host.

Using jexec to start Podman containers because they are actually jails,
with wrapper scripts and desktop entries to launch applications from your Freebsd application launcher.

So the applications arent run as root which would be the case if you used podman exec.

The uvm_ioctl_override.c and dummy-uvm.so are the hub for the wheel so to speak,
and without out them the i couldnt have added the spokes, rims and tyres.
 

I have a Podman container for Comfy-UI working using the dummy-uvm.so

 
[S]aider[/S] - this one is a bit trickier - git clone the source and then pip install in a venv and fix the dependency errors - it can be done but requires some kerfuffling

Edit:

Tried to simplify a bit.

The same task can - sometimes - be done without aider by using the llama-ccp built in web-ui. That means only install llama-cpp.

However, the llama-cpp built in web-ui will fill up the context window until you get a server error. The web-ui takes the server output and adds it to the context.

It is possible to get the models to do the same task as with aider but a large enough context window is required - meaning the task will to have to finish before the sum of the prompt and the server output into the web ui hits the size of the set context.

/grandpa
 
Podman allows you to set the platform and os in the compose.yaml file to linux.

Code:
platform: linux/amd64
os: linux

So it presents a "real" linux environment to applications.

If you use the Linuxulator or a Jail,
applications can see from sys calls that its actually running on Freebsd.

So for example if you try and download python torch with the Linuxulator or a Jail
then it will try and download Freebsd wheel files which dont exist.

The way my freebsd-cuda works is by using the updated version of shkhln dummy-uvm.so
to intercept calls to nvidia-uvm which doesnt exist on Freebsd.


I mount the Nvidia driver and linuxlator Rocky libraries into the Rocky linux Podman container.

Which means you dont have to manually install the same Nvidia driver version in the Podman container
as on the Freebsd host, because it sees the mounted Nvidia driver and libraries and uses them.

Because im using the exact same version of Rocky Linux in the container as in the Linuxulator,
there are no glib errors with the libraries.
 
You can use ffmpeg with Nvenc on Freebsd by installing shkhln's libc6-shim package

And then prefixing ffmpeg with nv-sglrun

Code:
nv-sglrun ffmpeg

shkhln correct me if i wrong,
but my understanding is programs have to be altered to work with the shim.

The advantage of using Rocky Linux Podman containers is that it doesnt require the libc6-shim package,
it just uses the dummy-uvm.so which means you you dont have to patch lots of programs to get them to work with Cuda or the gpu.

There are some programs that wont work with Podman and the Linuxlator,
due to missing sys calls like Python Jax

But i have managed to get the following working with Cuda/GPU

Davinci Resolve
ffmpeg
Comfy-UI
Demucs
Kokoro-onnx
Whisperx
Ollama
Blender
Firefox with hardware accelerated video and Widevine DRM support.

Providing the application doesnt require sys calls not supported by the Linuxulator,
then it should be possible to install it in a Rocky Linux Podman container

Like Handbrake for example.
 
I did say that the project depended on your work with the dummy-uvm.so

I did not link my work. And, in any case, my actual contribution here was diagnosing the missing piece in the driver and asking ashafer@ to fix it. All this focus on the trivial dummy-uvm stub is rather annoying.

OK, but is there a coherent explanation yet why CUDA works in containers but not directly in the Linuxulator?

It works just fine without this travesty. I personally tested it. My OpenCL and NVENC hacks rely on CUDA working: https://gist.github.com/shkhln/5996d9d4f60ef506a13730f607531b90.
 
libc6-shim is much lighter solution for Freebsd binaries,
im not disputing that

However Davinci Resolve isnt available on Freebsd,
and you couldn't create a Freebsd port because of licensing issues.

So you couldnt use libc6-shim.

Most of the open source video editors just arent as good as Davinci Resolve
and Kdenlive has a reputation for crashing a lot.

Having a Podman container you can build and install Davinci Resolve into,
means you have a professional video on editor on Freebsd thats relatively easy to install.

So this travesty does solve a problem of Freebsd not having a decent professional video editor.
 
From a previous conversation

Were you mentioned applications had to be patched to work with Cuda/GPU

FYI, there is no NVENC with Handbrake, because I didn't bother to submit the corresponding patches to the port. You are imagining things.


And my point in a previous post

shkhln correct me if i wrong,
but my understanding is programs have to be altered to work with the shim.

So unless things have changed since then,
using Podman container to get applications working with Cuda that havent been patched is solving a problem.
 
Back
Top