Skip to content

Commit e64d0f1

Browse files
authored
Merge pull request #5 from second-state/add-mlx-backend
Add Apple MLX backend for macOS
2 parents a52e59d + d4da7c1 commit e64d0f1

29 files changed

Lines changed: 3216 additions & 312 deletions

.github/workflows/ci.yml

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,34 @@ jobs:
1212
include:
1313
- os: ubuntu-latest
1414
name: Linux x86_64
15+
backend: tch
1516
libtorch-url: https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.7.1%2Bcpu.zip
1617
libtorch-archive: libtorch.zip
1718
- os: ubuntu-24.04-arm
1819
name: Linux ARM64
20+
backend: tch
1921
libtorch-url: https://github.com/second-state/libtorch-releases/releases/download/v2.7.1/libtorch-cxx11-abi-aarch64-2.7.1.tar.gz
2022
libtorch-archive: libtorch.tar.gz
2123
- os: macos-latest
22-
name: macOS ARM64
24+
name: macOS ARM64 (tch)
25+
backend: tch
2326
libtorch-url: https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-2.7.1.zip
2427
libtorch-archive: libtorch.zip
28+
- os: macos-latest
29+
name: macOS ARM64 (mlx)
30+
backend: mlx
2531

2632
runs-on: ${{ matrix.os }}
2733
name: Build (${{ matrix.name }})
2834

2935
steps:
3036
- uses: actions/checkout@v4
37+
if: matrix.backend == 'tch'
38+
39+
- uses: actions/checkout@v4
40+
if: matrix.backend == 'mlx'
41+
with:
42+
submodules: recursive
3143

3244
- name: Install build tools (Linux)
3345
if: runner.os == 'Linux'
@@ -42,14 +54,16 @@ jobs:
4254
path: |
4355
~/.cargo/registry
4456
~/.cargo/git
45-
key: ${{ runner.os }}-${{ runner.arch }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
57+
key: ${{ runner.os }}-${{ runner.arch }}-${{ matrix.backend }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
4658
restore-keys: |
47-
${{ runner.os }}-${{ runner.arch }}-cargo-registry-
59+
${{ runner.os }}-${{ runner.arch }}-${{ matrix.backend }}-cargo-registry-
4860
4961
- name: Download libtorch
62+
if: matrix.backend == 'tch'
5063
run: curl -Lo ${{ matrix.libtorch-archive }} "${{ matrix.libtorch-url }}"
5164

5265
- name: Extract libtorch
66+
if: matrix.backend == 'tch'
5367
run: |
5468
if [[ "${{ matrix.libtorch-archive }}" == *.zip ]]; then
5569
unzip -q ${{ matrix.libtorch-archive }}
@@ -58,21 +72,26 @@ jobs:
5872
fi
5973
6074
- name: Set linker rpath-link (Linux only)
61-
if: runner.os == 'Linux'
75+
if: matrix.backend == 'tch' && runner.os == 'Linux'
6276
run: echo "RUSTFLAGS=-C link-arg=-Wl,-rpath-link,${{ github.workspace }}/libtorch/lib" >> "$GITHUB_ENV"
6377

6478
- name: Cap libtorch CPU ISA to AVX2 (x86_64 only)
65-
if: runner.arch == 'X64'
79+
if: matrix.backend == 'tch' && runner.arch == 'X64'
6680
run: |
6781
echo "DNNL_MAX_CPU_ISA=AVX2" >> "$GITHUB_ENV"
6882
echo "ATEN_CPU_CAPABILITY=avx2" >> "$GITHUB_ENV"
6983
70-
- name: Build
84+
- name: Build (tch)
85+
if: matrix.backend == 'tch'
7186
env:
7287
LIBTORCH: ${{ github.workspace }}/libtorch
7388
LIBTORCH_BYPASS_VERSION_CHECK: "1"
7489
run: cargo build --release --features build-ffmpeg
7590

91+
- name: Build (mlx)
92+
if: matrix.backend == 'mlx'
93+
run: cargo build --release --no-default-features --features mlx,build-ffmpeg
94+
7695
- name: Set up Python
7796
uses: actions/setup-python@v5
7897
with:
@@ -82,11 +101,11 @@ jobs:
82101
run: pip install huggingface_hub transformers
83102

84103
- name: Set library path (Linux)
85-
if: runner.os == 'Linux'
104+
if: matrix.backend == 'tch' && runner.os == 'Linux'
86105
run: echo "LD_LIBRARY_PATH=${{ github.workspace }}/libtorch/lib:$LD_LIBRARY_PATH" >> "$GITHUB_ENV"
87106

88107
- name: Set library path (macOS)
89-
if: runner.os == 'macOS'
108+
if: matrix.backend == 'tch' && runner.os == 'macOS'
90109
run: echo "DYLD_LIBRARY_PATH=${{ github.workspace }}/libtorch/lib:$DYLD_LIBRARY_PATH" >> "$GITHUB_ENV"
91110

92111
- name: Cache 0.6B model weights
@@ -146,5 +165,5 @@ jobs:
146165
- name: Upload binary
147166
uses: actions/upload-artifact@v4
148167
with:
149-
name: qwen3-asr-${{ matrix.os }}-${{ runner.arch }}.zip
168+
name: qwen3-asr-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.backend }}.zip
150169
path: target/release/asr

.github/workflows/release.yml

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,26 @@ jobs:
1212
include:
1313
- os: ubuntu-latest
1414
name: Linux x86_64
15+
backend: tch
1516
asset-name: asr-linux-x86_64
1617
libtorch-url: https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.7.1%2Bcpu.zip
1718
libtorch-archive: libtorch.zip
19+
- os: ubuntu-latest
20+
name: Linux x86_64 (CUDA)
21+
backend: tch
22+
asset-name: asr-linux-x86_64-cuda
23+
libtorch-url: https://download.pytorch.org/libtorch/cu128/libtorch-cxx11-abi-shared-with-deps-2.7.1%2Bcu128.zip
24+
libtorch-archive: libtorch.zip
1825
- os: ubuntu-24.04-arm
1926
name: Linux ARM64
27+
backend: tch
2028
asset-name: asr-linux-aarch64
2129
libtorch-url: https://github.com/second-state/libtorch-releases/releases/download/v2.7.1/libtorch-cxx11-abi-aarch64-2.7.1.tar.gz
2230
libtorch-archive: libtorch.tar.gz
2331
- os: macos-latest
2432
name: macOS ARM64
33+
backend: mlx
2534
asset-name: asr-macos-aarch64
26-
libtorch-url: https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-2.7.1.zip
27-
libtorch-archive: libtorch.zip
2835

2936
runs-on: ${{ matrix.os }}
3037
name: Build (${{ matrix.name }})
@@ -34,6 +41,12 @@ jobs:
3441

3542
steps:
3643
- uses: actions/checkout@v4
44+
if: matrix.backend == 'tch'
45+
46+
- uses: actions/checkout@v4
47+
if: matrix.backend == 'mlx'
48+
with:
49+
submodules: recursive
3750

3851
- name: Install build tools (Linux)
3952
if: runner.os == 'Linux'
@@ -48,14 +61,16 @@ jobs:
4861
path: |
4962
~/.cargo/registry
5063
~/.cargo/git
51-
key: ${{ runner.os }}-${{ runner.arch }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
64+
key: ${{ runner.os }}-${{ runner.arch }}-${{ matrix.backend }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
5265
restore-keys: |
53-
${{ runner.os }}-${{ runner.arch }}-cargo-registry-
66+
${{ runner.os }}-${{ runner.arch }}-${{ matrix.backend }}-cargo-registry-
5467
5568
- name: Download libtorch
69+
if: matrix.backend == 'tch'
5670
run: curl -Lo ${{ matrix.libtorch-archive }} "${{ matrix.libtorch-url }}"
5771

5872
- name: Extract libtorch
73+
if: matrix.backend == 'tch'
5974
run: |
6075
if [[ "${{ matrix.libtorch-archive }}" == *.zip ]]; then
6176
unzip -q ${{ matrix.libtorch-archive }}
@@ -64,19 +79,25 @@ jobs:
6479
fi
6580
6681
- name: Set linker rpath-link (Linux only)
67-
if: runner.os == 'Linux'
82+
if: matrix.backend == 'tch' && runner.os == 'Linux'
6883
run: echo "RUSTFLAGS=-C link-arg=-Wl,-rpath-link,${{ github.workspace }}/libtorch/lib" >> "$GITHUB_ENV"
6984

70-
- name: Build
85+
- name: Build (tch)
86+
if: matrix.backend == 'tch'
7187
env:
7288
LIBTORCH: ${{ github.workspace }}/libtorch
7389
LIBTORCH_BYPASS_VERSION_CHECK: "1"
7490
run: cargo build --release --features build-ffmpeg
7591

92+
- name: Build (mlx)
93+
if: matrix.backend == 'mlx'
94+
run: cargo build --release --no-default-features --features mlx,build-ffmpeg
95+
7696
- name: Package binary
7797
run: |
78-
cp target/release/asr ${{ matrix.asset-name }}
79-
zip ${{ matrix.asset-name }}.zip ${{ matrix.asset-name }}
98+
mkdir ${{ matrix.asset-name }}
99+
cp target/release/asr ${{ matrix.asset-name }}/asr
100+
zip -r ${{ matrix.asset-name }}.zip ${{ matrix.asset-name }}
80101
81102
- name: Upload release asset
82103
env:

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "mlx-c"]
2+
path = mlx-c
3+
url = https://github.com/ml-explore/mlx-c.git

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ name = "asr"
1010
path = "src/main.rs"
1111

1212
[dependencies]
13-
tch = "0.20"
13+
tch = { version = "0.20", optional = true }
1414
tokenizers = "0.21"
1515
serde = { version = "1.0", features = ["derive"] }
1616
serde_json = "1.0"
@@ -24,7 +24,12 @@ safetensors = "0.5"
2424
ffmpeg-next = "8"
2525
ffmpeg-sys-next = "8"
2626

27+
[build-dependencies]
28+
cmake = "0.1"
29+
2730
[features]
28-
default = []
31+
default = ["tch-backend"]
32+
tch-backend = ["dep:tch"]
33+
mlx = []
2934
static-ffmpeg = ["ffmpeg-sys-next/static"]
3035
build-ffmpeg = ["ffmpeg-sys-next/build", "static-ffmpeg"]

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Qwen3 ASR Rust
22

3-
Pure Rust implementation of [Qwen3-ASR](https://github.com/QwenLM/Qwen3-ASR) automatic speech recognition using libtorch. Loads model weights directly from safetensors files and re-implements the complete neural network forward pass in Rust.
3+
Pure Rust implementation of [Qwen3-ASR](https://github.com/QwenLM/Qwen3-ASR) automatic speech recognition. Supports two backends: **libtorch** (via the `tch` crate, cross-platform with optional CUDA) and **MLX** (Apple Silicon native via Metal GPU). Loads model weights directly from safetensors files and re-implements the complete neural network forward pass in Rust.
44

55
## Architecture
66

@@ -19,7 +19,16 @@ The implementation ports the Qwen3-ASR encoder-decoder architecture from PyTorch
1919

2020
## Prerequisites
2121

22-
### libtorch
22+
### Backend
23+
24+
Choose one backend:
25+
26+
| Backend | Feature flag | Platforms | GPU |
27+
|---------|-------------|-----------|-----|
28+
| libtorch | `tch-backend` (default) | Linux, macOS, Windows | CUDA |
29+
| MLX | `mlx` | macOS Apple Silicon | Metal |
30+
31+
### libtorch (for `tch-backend`)
2332

2433
The `tch` crate (v0.20) requires **libtorch 2.7.1**. Download and extract for your platform:
2534

@@ -77,6 +86,8 @@ tok.backend_tokenizer.save('Qwen3-ASR-0.6B/tokenizer.json')
7786

7887
## Build
7988

89+
### libtorch backend (default)
90+
8091
```bash
8192
# Set environment
8293
export LIBTORCH=$(pwd)/libtorch
@@ -94,6 +105,19 @@ cargo build --release --features static-ffmpeg
94105
cargo build --release --features build-ffmpeg
95106
```
96107

108+
### MLX backend (macOS Apple Silicon)
109+
110+
```bash
111+
# Initialize mlx-c submodule
112+
git submodule update --init --recursive
113+
114+
# Build with MLX (no libtorch needed)
115+
cargo build --release --no-default-features --features mlx
116+
117+
# With statically linked FFmpeg
118+
cargo build --release --no-default-features --features mlx,static-ffmpeg
119+
```
120+
97121
## Usage
98122

99123
```bash
@@ -134,6 +158,7 @@ Qwen3-ASR supports 30 languages: Chinese, English, Cantonese, Arabic, German, Fr
134158
src/
135159
├── main.rs # CLI binary entry point
136160
├── lib.rs # Library module declarations
161+
├── tensor.rs # Unified Tensor abstraction (tch/MLX backend)
137162
├── config.rs # Model configuration (from config.json)
138163
├── error.rs # Error types
139164
├── audio.rs # FFmpeg-based audio loading and format conversion
@@ -144,7 +169,15 @@ src/
144169
├── audio_encoder.rs # Whisper-style audio encoder (Conv2d + Transformer)
145170
├── text_decoder.rs # Qwen3 text decoder with KV cache
146171
├── tokenizer.rs # HuggingFace tokenizer wrapper
147-
└── inference.rs # End-to-end ASR inference pipeline
172+
├── inference.rs # End-to-end ASR inference pipeline
173+
└── backend/
174+
└── mlx/ # Apple MLX backend (Metal GPU)
175+
├── ffi.rs # Raw C FFI bindings to mlx-c
176+
├── array.rs # Safe RAII MlxArray wrapper
177+
├── ops.rs # Safe operation wrappers
178+
├── io.rs # Safetensors loading via mlx-c
179+
├── signal.rs # STFT, mel spectrogram signal processing
180+
└── stream.rs # Device/stream management
148181
```
149182

150183
## License

0 commit comments

Comments
 (0)