
Over the weekend I was reading this post on the Oracle Linux Blog.
It looked pretty simple, so I thought I would give it a go, and that lead me down a rabbit hole for the next few days. 🙂
Why would anyone want to run LLMs locally?
There are potential issues with using a cloud-based LLM.
- Security: Passing sensitive data or your company intellectual property to the cloud could be a problem. Using a local LLM means you can do pretty much what you want with it. It also means your prompts are not being stored and available to the outside world when the provider of your cloud-based LLM inevitably gets hacked, or sells your details.
- Consistency: The cloud vendors update their models from time to time, and you may find a “new and improved” version of a model works less well for what you are doing. Just look what the “upgrade” to ChatGPT 5 broke. Using a local LLM means you are in control of if/when you update models.
- Cost: You have the cost of the local kit, but after that it’s all “free”. Running a model requires a lot less kit than training a model. Depending on your needs, maybe running something locally will make more sense than paying for a cloud subscription. It can get really expensive if you require a large call volume on a cloud subscription.
Apart from anything else, it’s just fun, which is why I’m doing it. 🙂
My (Bad) Kit
I don’t have any suitable kit for this, and I don’t like installing things directly on my laptop if I can help it, so I decided to use a minimum installation of Oracle Linux 9 on VirtualBox. I built it using Vagrant, but more about that later. I subsequently tried OL10 and Fedora 43 with similar results.
In the end the VM specs were as follows.
- OS: OL9
- vCPUs: 4
- RAM:10240M (or 16384M for for gpt-oss:20b and codestral)
During the Ollama installation I got this message.
WARNING: No NVIDIA/AMD GPU detected. Ollama will run in CPU-only mode.Suffice to say, this was not going to perform well, but I didn’t care. I just wanted to see it running locally. Clearly if you have better kit, and can throw your GPU and loads of memory at it, it’s going to work better!
Basic Installation
Ollama is a tool that allows you to work with a range of open Large Language Models (LLMs). It’s not a model itself. Just a tool that allows you to work with them in a consistent manner.
Ollama is available for Linux, Windows and Mac, so you can install it on just about anything. If you want to download it for Windows and Mac, you can get it from here.
On Linux we can install it using a single command. The basic installation worked fine, but there were some subsequent issues. This command does the whole installation, and creates a Linux service called “ollama”.
curl -fsSL https://ollama.com/install.sh | shI set up port forwarding in the VM for port 11434, so I expected to be able to access the Ollama API from my host machine, but that didn’t work. After a little digging I found out that the “ollama” service is limited to localhost by default, so you can’t access it externally. To fix this I went nuclear and added the following to the “ollama” service config file (/etc/systemd/system/ollama.service).
Environment="OLLAMA_HOST=0.0.0.0:11434"That means any network can access it, so you would need to protect it with a local or network firewall if you were doing this for real.
With Ollama in place, it was time to pick a model. I started by using llama3, as they did in the blog post.
ollama pull llama3It took a while to download, but as soon as it was, it was visible in my list of models using this API URL from curl in a terminal, or from a browser on the host machine.
http://localhost:11434/api/tagsThere are a couple of ways to interact with the model. You can use the command line interface to speak to it by running this command.
ollama run llama3Alternatively, you can issue the questions through the API, in streaming or non-streaming mode.
# Streaming.
curl http://localhost:11434/api/generate -d '{
"model": "llama3",
"prompt": "In less than 50 words, what is Star Trek about?"
}'
# Non-streaming.
curl http://localhost:11434/api/generate -d '{
"model": "llama3",
"prompt": "In less than 50 words, what is Star Trek about?",
"stream": false
}'
The API works fine, but the output is not ideal from a readability perspective, as they are expecting you to interact with it programmatically and format the output. That lead to the next rabbit hole.
Open WebUI
The blog post mentioned using Open WebUI as an interface. I tried the suggested installation, but got into the weeds with conflicting package versions. Instead I decided to use the Open WebUI container, which made life a lot easier. I installed Podman and started the container like this.
dnf install -y podman
podman pull ghcr.io/open-webui/open-webui:main
mkdir -p /u01/open-webui
podman run -d -p 8080:8080 \
-v /u01/open-webui:/app/backend/data \
--name open-webui \
-e OLLAMA_HOST=host.containers.internal \
-e OLLAMA_PORT=11434 \
--add-host host.containers.internal:host-gateway \
ghcr.io/open-webui/open-webui:mainNotice I had to expose the host machines localhost to the container, where it could be referenced as “host.containers.internal”. This was then set in the OLLAMA_HOST environment variable inside the container.
The combination of exposing the host localhost to the container and opening up Ollama’s API to external access took far too much messing about to sort out. As always, it’s easy when you know how. 🙂
I now had a neat web interface to access any models using this URL.
http://localhost:8080/Which models?
As mentioned previously, I began with the llama3 model, but then I started to compare several other models.
# General models.
ollama pull llama3
ollama pull deepseek-r1
ollama pull gpt-oss:20b
# Code-focussed models.
ollama pull codellama
ollama pull deepseek-coder
ollama pull codestralThis gave me the choice of 3 general models, and 3 code-focussed models. I picked the smaller ChatGPT model, and it still required more memory than the others. I don’t think I could have run the gpt-oss:120b model.
Vagrant
As mentioned previously, I built my playground using Vagrant, and you can see what I did here.
The VM itself is just a simple shell. There are two setup scripts you can run once connected as root. I left them as manual calls so I could mess around with them during each build. Their calls are commented out in the “root_setup.sh” file, but you could put them back in if you prefer.
ssh vagrant@localhost -p 2222
sudo su -
sh /vagrant/scripts/setup_ollama.sh
sh /vagrant/scripts/setup_open_webui.shIf I included them in the build directly, you would be waiting a long time for the VM build to complete, and I didn’t want that. 🙂
I’ve left this as simple as possible. It’s not meant as an example of the best way to build this. You have been warned. 🙂
What did I do with it?
I was speaking to a colleague about it, and he said Claude Code was baked into his IDE (JetBrains) and he had used it recently to help with some C# code. He used to be a C# developer, but hadn’t done much over the last few years, so he was asking it some basic questions to remind himself. Since his was a “real” example of what someone might ask, I thought it might be a good idea to compare the results. You can see the comparison for a simple question in the section at the end if you are interested.
Overall Thoughts
Ollama works fine. It’s easy to setup and gives access to a whole bunch of models to play around with. After a bit of fiddling, Open WebUI worked fine and gave a much nicer interface than using the command line, or the API directly. It was worth the hassle.
Clearly running LLMs locally on my old laptop inside a VM was terribly slow, and would drive you crazy if you were using it for something real, but at least it allows you to get your hands dirty. If you have some more modern kit, with a compatible GPU and more memory to throw at it, it’s going to be a lot more useable. And remember, running a model takes a lot less kit than training a model. As you can see here, I’m able to run an existing model on junk kit.
I don’t know what I’m going to do with this going forward. Maybe nothing, but it was fun to play around with.
That’s it really. If you want to look at a bunch of output from varying models for the same simple question, you can see that below, but I won’t be offended if you skip the rest of the post. 🙂
And before you ask, I’m not trying to pivot into being an AI guru, but if Zuck wants to pay me $300M to play around with this stuff, I might consider it. 🙂
Cheers
Tim…
Basic Comparison
Some things to remember before we start.
- The format of the output varies a lot when I am pasting it as text here, but typically it is formatted neatly on screen, usually like a markdown page, so ignore the formatting issues here.
- The time it took to return an answer varied wildly. I’m not sure how much of that was due to the models, and how much of that was due to my laptop going on strike. As a result I ignored the timings and just looked at the output. With some better kit it would be nice to compare timings between models.
- The runs of the local LLMs took so long I did very limited testing. 🙂
This was one of his questions.
“how do you join 2 lists in C#”
Claude Code is the cloud solution my colleague used, so it was pretty quick and gave him this.
In C#, there are several ways to join (concatenate) two lists:
1. Using Concat() (LINQ)
The most common and readable approach:
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5, 6 };
var combined = list1.Concat(list2).ToList();
// Result: { 1, 2, 3, 4, 5, 6 }
2. Using AddRange()
Modifies the original list (no new list created):
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5, 6 };
list1.AddRange(list2);
// list1 is now: { 1, 2, 3, 4, 5, 6 }
3. Using Collection Expression (C# 12+)
The newest and most concise syntax:
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5, 6 };
var combined = [..list1, ..list2];
// Result: { 1, 2, 3, 4, 5, 6 }
4. Using Union() (removes duplicates)
If you want to avoid duplicate values:
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 3, 4, 5 };
var combined = list1.Union(list2).ToList();
// Result: { 1, 2, 3, 4, 5 } - note: 3 appears only once
Quick Comparison
Method Creates New List Preserves Duplicates Performance
Concat() Yes Yes Good
AddRange() No (modifies original) Yes Best
[..list1, ..list2] Yes Yes Good
Union() Yes No Slower
Choose based on your needs:
Need to preserve both original lists? Use Concat() or collection expressions [..]
Want to modify the first list? Use AddRange()
Need to remove duplicates? Use Union()
The llama3 model produced the following.
There are several ways to join two lists in C#. Here are a few examples:
Using the Concat method:
List<string> list1 = new List<string> { "a", "b" };
List<string> list2 = new List<string> { "c", "d" };
List<string> joinedList = list1.Concat(list2).ToList();
// joinedList now contains: "a", "b", "c", "d"
The Concat method takes two sequences (in this case, lists) and returns a new sequence that contains the elements from both lists.
Using the Union method:
List<string> list1 = new List<string> { "a", "b" };
List<string> list2 = new List<string> { "c", "d" };
List<string> joinedList = list1.Union(list2).ToList();
// joinedList now contains: "a", "b", "c", "d"
The Union method takes two sequences and returns a new sequence that contains all the unique elements from both lists.
Using LINQ:
List<string> list1 = new List<string> { "a", "b" };
List<string> list2 = new List<string> { "c", "d" };
var joinedList = (from e in list1 select e).Concat(from e in list2 select e).ToList();
// joinedList now contains: "a", "b", "c", "d"
This uses LINQ to concatenate the two lists.
Using a simple loop:
List<string> list1 = new List<string> { "a", "b" };
List<string> list2 = new List<string> { "c", "d" };
List<string> joinedList = new List<string>();
foreach (string s in list1)
{
joinedList.Add(s);
}
foreach (string s in list2)
{
joinedList.Add(s);
}
// joinedList now contains: "a", "b", "c", "d"
This method is straightforward, but less elegant than the other options.
Note that these examples assume you want to concatenate two lists of strings. If you have lists of different types or more complex structures, you may need to use a different approach.The deepseek-r1 model produced the following.
To join two lists in C#, you can use the following methods depending on your needs:
1. **Concatenate Two Lists (Combine into One List)**
Use **`List<T>.AddRange()`** to merge two lists:
List<int> list1 = new List<int> { 1, 2 };
List<int> list2 = new List<int> { 3, 4 };
List<int> combined = new List<int>();
combined.AddRange(list1); // Add elements from list1
combined.AddRange(list2); // Add elements from list2
// Result: combined = [1, 2, 3, 4]
### 2. **Join Lists into a Single String (Element + Separator)**
Use **`string.Join()`** to concatenate elements with a separator:
List<int> list1 = new List<int> { 1, 2 };
List<int> list2 = new List<int> { 3, 4 };
List<int> combinedList = list1.Concat(list2).ToList(); // Combine into one list
string result = string.Join(", ", combinedList);
// Result: "1, 2, 3, 4"
3. **Combine Elements from Two Lists (Element-by-Element)**
Use **`Enumerable.Zip()`** to merge elements from two lists (requires same length):
List<int> listA = new List<int> { 1, 2 };
List<int> listB = new List<int> { 3, 4 };
var combined = listA.Zip(listB, (a, b) => $"{a}-{b}").ToList();
// Result: combined = ["1-3", "2-4"]
4. **Merge Lists with Distinct Elements**
Use **`Enumerable.Concat()`** + **`Distinct()`** to avoid duplicates:
List<int> list1 = new List<int> { 1, 2 };
List<int> list2 = new List<int> { 2, 3 };
var merged = list1.Concat(list2).Distinct().ToList();
// Result: merged = [1, 2, 3]
Key Notes:
- **`AddRange()`** is ideal for merging **`List<T>`** objects directly.
- **`string.Join()`** requires elements to be **`IEnumerable<T>`** (e.g., strings, numbers).
- **`Zip()`** works only if both lists have the same number of elements.
- **`Concat()`** creates an **`IEnumerable<T>`**; use `.ToList()` to convert to a `List<T>`.
Choose the method based on whether you need a combined list or a joined string.The gpt-oss:20b model produced the following.
Joining two lists in C#
What you probably mean by “join” is concatenating (or appending) the elements of two lists into a single list.
(If you meant a relational join by key, that’s a different story – see the section at the bottom.)
Below are the most common ways to do it, with code snippets and a few gotchas.
1. Using AddRange (in?place)
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5, 6 };
list1.AddRange(list2); // list1 now contains 1,2,3,4,5,6
// list2 stays unchanged
Pros
Very fast – no allocations beyond the destination list.
Modifies the original list, so you don’t need a temporary variable.
Cons
list1 will grow; if you need a new list you must create one first.
2. Using LINQ Concat
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5, 6 };
var combined = list1.Concat(list2).ToList(); // new list
Pros
Concise and expressive.
Works with any IEnumerable<T> – not just List<T>.
You get a new list, leaving the originals untouched.
Cons
Allocates a new list ( ToList()) – may be overkill if you just want a quick append.
Slight overhead from LINQ plumbing (but negligible for typical list sizes).
3. Using List<T>.AddRange on a freshly?created list
If you want a new list and prefer the imperative style:
var combined = new List<int>(list1.Count + list2.Count);
combined.AddRange(list1);
combined.AddRange(list2);
This pre?allocates the correct capacity, so the list won’t have to grow as you add elements.
4. Using Enumerable.Append in a loop (rare)
If you only have a few items or need to do some processing per item:
var result = list1.Select(x => x); // start with list1
foreach (var item in list2)
{
result = result.Append(item);
}
var combined = result.ToList();
Generally not recommended unless you have a very specific reason.
5. Union vs. Concat
Concat: Keeps all items, including duplicates.
Union: Returns distinct items, preserving the order of the first list then the second.
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 3, 4, 5 };
var union = list1.Union(list2).ToList(); // 1,2,3,4,5
If you only need to avoid duplicates, Union is the way to go.
6. “Join” by key (relational join
Sometimes people use the term join to mean combining two collections based on a common key, just like a SQL join. In C#, that’s usually done with LINQ’s Join, GroupJoin, or SelectMany. Here’s a quick example:
var customers = new[]
{
new { Id = 1, Name = "Alice" },
new { Id = 2, Name = "Bob" }
};
var orders = new[]
{
new { Id = 101, CustomerId = 1, Amount = 50m },
new { Id = 102, CustomerId = 1, Amount = 25m },
new { Id = 103, CustomerId = 2, Amount = 75m }
};
var customerOrders = from c in customers
join o in orders on c.Id equals o.CustomerId
select new { c.Name, o.Id, o.Amount };
foreach (var co in customerOrders)
{
Console.WriteLine($"{co.Name} ordered #{co.Id} for {co.Amount:C}");
}
Output:
Alice ordered #101 for $50.00
Alice ordered #102 for $25.00
Bob ordered #103 for $75.00
Inner join ( join … on … equals …) – only matching pairs.
Left outer join ( GroupJoin + SelectMany with DefaultIfEmpty) – keeps all left items.
Full outer join – requires a bit more plumbing (you typically need to union the two sides).
If you’re after a relational join, choose the appropriate LINQ method ( Join, GroupJoin, SelectMany, DefaultIfEmpty, etc.).
Quick Reference Table
Scenario
Method
Result
Append two lists into one new list
list1.Concat(list2).ToList()
New list, duplicates kept
Append in-place
list1.AddRange(list2)
list1 grows, list2 unchanged
Append without duplicates
list1.Union(list2).ToList()
New list, duplicates removed
Relational join by key
`list1.Join(list2The codellama model produced the following.
There are several ways to join two lists in C#, depending on the specific context and requirements. Here are a few common methods:
1. Using the `Concat` method:
List<int> list1 = new List<int>() { 1, 2, 3 };
List<int> list2 = new List<int>() { 4, 5, 6 };
var joinedList = list1.Concat(list2).ToList();
This will concatenate the two lists and create a new `List<int>` object that contains all the elements from both lists.
2. Using the `Join` method:
List<int> list1 = new List<int>() { 1, 2, 3 };
List<int> list2 = new List<int>() { 4, 5, 6 };
var joinedList = list1.Join(list2, i => i, j => j, (i, j) => new { First = i, Second = j });
This will join the two lists based on a common property and create a new `IEnumerable<T>` object that contains all the elements from both lists, with each element containing the properties of the two lists.
3. Using the `Union` method:
List<int> list1 = new List<int>() { 1, 2, 3 };
List<int> list2 = new List<int>() { 4, 5, 6 };
var joinedList = list1.Union(list2).ToList();
This will combine the two lists and create a new `List<int>` object that contains all the unique elements from both lists.
4. Using the `Intersect` method:
List<int> list1 = new List<int>() { 1, 2, 3 };
List<int> list2 = new List<int>() { 4, 5, 6 };
var joinedList = list1.Intersect(list2).ToList();
This will find the common elements in both lists and create a new `List<int>` object that contains only those elements.
These are just a few examples of how you can join two lists in C#. There are many other ways to do this, depending on your specific needs and requirements.The deepseek-coder model produced the following.
In C#, the Join method can be used for combining two collections into one string or another collection type that implements IEnumerable<T> (like List). Here is an example of how it works on a list. Let's say we have these following code snippets in your project and you want to join them:
1) Using the Join method from LINQ(Language Integrated Query):
List<string> names = new List<string> { "John", "Alice" };
var joinedNames = string.Join(" ", names); // Outputs: John Alice
Console.WriteLine(joinedNames);
2) Using the Join method manually (Without LINQ):
If you want to join two collections without using any Linq, then it'll be a bit more manual operation and requires knowledge of looping mechanism in C# like for-each loops or foreach. Here is an example:
List<string> names1 = new List<string> { "John", "Alice" }; // First list
List<string> names2=new List<string> {"Bob","Charlie"} ;// Second list to join the first one.
names1.AddRange(names2);
Console.WriteLine("Joined Result: {0}", string.Join (" ", names1)); // Outputs : John Alice Bob Charlie
In both cases, this operation will generate a new List that contains all items from the two lists (one after another), and then it'll join these elements with spaces between them using `string.Join` method which returns final joined string/collection of strings in case if you want to print out combined list as output for instance console application or web API controller response header content values, etc..The codestral model produced the following.
You can join two lists in C# by using the `AddRange()` method of the `List<T>` class. This method adds a collection to the end of the existing list.
Here is an example:
// Declare and initialize the first list
List<string> list1 = new List<string> { "one", "two", "three" };
// Declare and initialize the second list
List<string> list2 = new List<string> { "four", "five", "six" };
// Use AddRange to join the two lists
list1.AddRange(list2);
After executing this code, `list1` will contain all elements from both `list1` and `list2`.There was a lot of similarity between the output from the models, but there were some differences. Specifically, the only model that suggested a solution using a Collection Expression was Claude Code, which was interesting.







