Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
"$(grep -m1 '^version' packages/rust/crates/oxbitnet-python/pyproject.toml | sed 's/.*"\(.*\)"/\1/')"
check packages/rust/crates/oxbitnet-java/java/build.gradle.kts \
"$(grep -m1 '^version' packages/rust/crates/oxbitnet-java/java/build.gradle.kts | sed 's/.*"\(.*\)"/\1/')"
check packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj \
"$(grep '<Version>' packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj | sed 's/.*<Version>\(.*\)<\/Version>/\1/' | tr -d ' ')"
exit $ERRORS

# ── crates.io ──
Expand Down Expand Up @@ -307,3 +309,82 @@ jobs:
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PASSPHRASE }}

# ── NuGet (.NET / C#) ──
build-nuget-natives:
needs: version-check
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
rid: linux-x64
lib: liboxbitnet_ffi.so
- os: macos-latest
target: aarch64-apple-darwin
rid: osx-arm64
lib: liboxbitnet_ffi.dylib
- os: macos-latest
target: x86_64-apple-darwin
rid: osx-x64
lib: liboxbitnet_ffi.dylib
- os: windows-latest
target: x86_64-pc-windows-msvc
rid: win-x64
lib: oxbitnet_ffi.dll
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/rust

- name: Build native library
working-directory: packages/rust
run: cargo build -p oxbitnet-ffi --release --target ${{ matrix.target }}

- uses: actions/upload-artifact@v4
with:
name: nuget-native-${{ matrix.rid }}
path: packages/rust/target/${{ matrix.target }}/release/${{ matrix.lib }}

publish-nuget:
needs: build-nuget-natives
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

- name: Download native libraries
uses: actions/download-artifact@v4
with:
pattern: nuget-native-*
path: native-tmp

- name: Bundle natives into runtimes
run: |
CSHARP_DIR=packages/rust/crates/oxbitnet-csharp
for dir in native-tmp/nuget-native-*; do
RID=$(basename "$dir" | sed 's/nuget-native-//')
DEST="$CSHARP_DIR/runtimes/$RID/native"
mkdir -p "$DEST"
cp "$dir"/* "$DEST/"
done

- name: Pack and publish
working-directory: packages/rust/crates/oxbitnet-csharp/src/OxBitNet
run: |
dotnet pack -c Release -o ../../nupkg
dotnet nuget push ../../nupkg/*.nupkg \
--api-key ${{ secrets.NUGET_API_KEY }} \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
25 changes: 25 additions & 0 deletions packages/rust/crates/oxbitnet-csharp/OxBitNet.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.0.0
MinimumVisualStudioVersion = 10.0.0.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OxBitNet", "src\OxBitNet\OxBitNet.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatConsole", "examples\ChatConsole\ChatConsole.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
122 changes: 122 additions & 0 deletions packages/rust/crates/oxbitnet-csharp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# oxbitnet-csharp

C# / .NET bindings for [oxbitnet](https://crates.io/crates/oxbitnet) — run [BitNet b1.58](https://github.com/microsoft/BitNet) ternary LLMs with GPU acceleration (wgpu).

Part of [0xBitNet](https://github.com/m96-chan/0xBitNet).

## Build

First, build the native library:

```bash
cargo build -p oxbitnet-ffi --release
```

Produces `target/release/liboxbitnet_ffi.so` (Linux) / `.dylib` (macOS) / `oxbitnet_ffi.dll` (Windows).

Then build the C# project:

```bash
cd packages/rust/crates/oxbitnet-csharp
dotnet build
```

## Quick Start

```csharp
using OxBitNet;

// Load a model
using var model = BitNet.LoadSync("model.gguf");

// Raw prompt
model.Generate("Hello!", token => Console.Write(token));

// Chat messages
model.Chat(new[] {
ChatMessage.User("Hello!")
}, token => Console.Write(token), new GenerateOptions { Temperature = 0.7f });
```

## API

### Loading

```csharp
// Sync (blocks calling thread)
using var model = BitNet.LoadSync("model.gguf");

// Async
using var model = await BitNet.Load("model.gguf");

// With progress
using var model = BitNet.LoadSync("model.gguf", new LoadOptions {
OnProgress = p => Console.WriteLine($"[{p.Phase}] {p.Fraction * 100:F1}%")
});
```

### Generation

```csharp
// Raw prompt — tokens delivered via callback
model.Generate("Once upon a time", token => Console.Write(token));

// With options
model.Generate("Hello!", token => Console.Write(token), new GenerateOptions {
MaxTokens = 512,
Temperature = 0.7f,
TopK = 40,
});

// Async variant
await model.GenerateAsync("Hello!", token => Console.Write(token));
```

### Chat

```csharp
var messages = new[] {
ChatMessage.System("You are a helpful assistant."),
ChatMessage.User("What is 2+2?"),
};

model.Chat(messages, token => Console.Write(token));

// Async variant
await model.ChatAsync(messages, token => Console.Write(token));
```

### Cleanup

`BitNet` implements `IDisposable`. Use `using` statements or call `Dispose()` explicitly:

```csharp
model.Dispose();
```

## Generation Options

| Field | Default | Description |
|-------|---------|-------------|
| `MaxTokens` | 256 | Maximum tokens to generate |
| `Temperature` | 1.0 | Sampling temperature |
| `TopK` | 50 | Top-k sampling |
| `RepeatPenalty` | 1.1 | Repetition penalty |
| `RepeatLastN` | 64 | Window for repetition penalty |

## Unity

OxBitNet targets `netstandard2.1` for Unity 2021.2+ compatibility. Place the native library in your Unity project's `Plugins` folder — Unity's plugin system handles native loading automatically.

## Running the Example

```bash
cd packages/rust
cargo build -p oxbitnet-ffi --release
cd crates/oxbitnet-csharp
dotnet run --project examples/ChatConsole -- /path/to/model.gguf
```

## License

MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<LangVersion>9.0</LangVersion>
<RootNamespace>ChatConsole</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../../src/OxBitNet/OxBitNet.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using OxBitNet;

class Program
{
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.Error.WriteLine("Usage: ChatConsole <model-path>");
Environment.Exit(1);
}

string modelPath = args[0];

Console.Error.WriteLine($"Loading {modelPath}...");
using var model = BitNet.LoadSync(modelPath, new LoadOptions
{
OnProgress = p =>
{
Console.Error.WriteLine($" [{p.Phase}] {p.Fraction * 100:F1}%");
}
});
Console.Error.WriteLine("Model loaded.");

var history = new List<ChatMessage>();
Console.WriteLine("Type a message (or 'quit' to exit):");

while (true)
{
Console.Write("\n> ");
string input = Console.ReadLine();
if (input == null || input.Trim().Equals("quit", StringComparison.OrdinalIgnoreCase))
break;

if (string.IsNullOrWhiteSpace(input))
continue;

history.Add(ChatMessage.User(input));

Console.Write("\n");
var response = new System.Text.StringBuilder();

model.Chat(history.ToArray(), token =>
{
Console.Write(token);
response.Append(token);
}, new GenerateOptions { Temperature = 0.7f });

Console.WriteLine();
history.Add(ChatMessage.Assistant(response.ToString()));
}
}
}
Loading