Skip to content

Commit 8c13080

Browse files
committed
Split CLI inference modes
1 parent c3765e3 commit 8c13080

5 files changed

Lines changed: 163 additions & 54 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ edition = "2024"
55
license = "GPL-3.0-only"
66
description = "Run koharu-torch inference graphs from RustScript host functions"
77

8+
[[bin]]
9+
name = "flint-ai"
10+
path = "src/bin/flint-ai.rs"
11+
812
[dependencies]
913
anyhow = "1"
1014
clap = { version = "4.5", features = ["derive"] }

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ A typical integration follows this flow:
2929
3. Pass the compiled program and string arguments to `run_text`.
3030
4. Read the published text from `ScriptTextOutput`.
3131

32+
## CLI
33+
34+
The `flint-ai` binary has explicit modes:
35+
36+
```text
37+
flint-ai --llm --script scripts/lfm2.rss [--device cuda:0] <args...>
38+
flint-ai --lama --weights model.safetensors --image input.png --mask mask.png --output output.png [--device cuda:0]
39+
```
40+
41+
When `--device` is omitted, the CLI initializes LibTorch and selects `cuda:0`
42+
when CUDA is available. Passing `--device` overrides that selection.
43+
3244
## Host functions
3345

3446
All functions are registered under the `flint` namespace.

src/bin/flint-ai.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use std::path::{Path, PathBuf};
2+
use std::sync::Arc;
3+
4+
use anyhow::{Context, Result, anyhow, bail};
5+
use clap::{ArgAction, Parser};
6+
use flint_ai::{LamaRustScript, TorchScriptRunner, preload_libtorch, resolve_device};
7+
use vm::compile_source;
8+
9+
#[derive(Debug, Parser)]
10+
#[command(about = "Run Flint inference programs")]
11+
struct Cli {
12+
#[arg(long, action = ArgAction::SetTrue, conflicts_with = "lama")]
13+
llm: bool,
14+
15+
#[arg(long, action = ArgAction::SetTrue, conflicts_with = "llm")]
16+
lama: bool,
17+
18+
#[arg(long, value_name = "DEVICE")]
19+
device: Option<String>,
20+
21+
#[arg(long, value_name = "FILE")]
22+
script: Option<PathBuf>,
23+
24+
#[arg(long, value_name = "FILE")]
25+
weights: Option<PathBuf>,
26+
27+
#[arg(long, value_name = "FILE")]
28+
image: Option<PathBuf>,
29+
30+
#[arg(long, value_name = "FILE")]
31+
mask: Option<PathBuf>,
32+
33+
#[arg(long, value_name = "FILE")]
34+
output: Option<PathBuf>,
35+
36+
#[arg(value_name = "ARG", trailing_var_arg = true)]
37+
args: Vec<String>,
38+
}
39+
40+
#[tokio::main]
41+
async fn main() -> Result<()> {
42+
let cli = Cli::parse();
43+
preload_libtorch().await?;
44+
let device = resolve_device(cli.device.as_deref())?;
45+
46+
match (cli.llm, cli.lama) {
47+
(true, false) => run_llm(device, cli.script, cli.args).await,
48+
(false, true) => run_lama(device, cli.weights, cli.image, cli.mask, cli.output).await,
49+
_ => bail!("choose one mode: --llm or --lama"),
50+
}
51+
}
52+
53+
async fn run_llm(
54+
device: koharu_torch::Device,
55+
script: Option<PathBuf>,
56+
args: Vec<String>,
57+
) -> Result<()> {
58+
let script = required_path(script, "--script")?;
59+
let source = std::fs::read_to_string(&script)
60+
.with_context(|| format!("failed to read {}", script.display()))?;
61+
let compiled = compile_source(&source)
62+
.map_err(|err| anyhow!("failed to compile {}: {err}", script.display()))?;
63+
let runner = TorchScriptRunner::new(device).await?;
64+
let output = runner.run_text(Arc::new(compiled.program), args)?;
65+
if !output.text.is_empty() {
66+
println!("{}", output.text);
67+
}
68+
print_token_rates(&output);
69+
Ok(())
70+
}
71+
72+
async fn run_lama(
73+
device: koharu_torch::Device,
74+
weights: Option<PathBuf>,
75+
image: Option<PathBuf>,
76+
mask: Option<PathBuf>,
77+
output: Option<PathBuf>,
78+
) -> Result<()> {
79+
let weights = required_path(weights, "--weights")?;
80+
let image_path = required_path(image, "--image")?;
81+
let mask_path = required_path(mask, "--mask")?;
82+
let output_path = required_path(output, "--output")?;
83+
84+
let image = image::open(&image_path)
85+
.with_context(|| format!("failed to read image {}", image_path.display()))?;
86+
let mask = image::open(&mask_path)
87+
.with_context(|| format!("failed to read mask {}", mask_path.display()))?
88+
.to_luma8();
89+
90+
let model = LamaRustScript::new(device).await?;
91+
let result = model.inference(&weights, &image, &mask)?;
92+
ensure_parent_dir(&output_path)?;
93+
result
94+
.save(&output_path)
95+
.with_context(|| format!("failed to write {}", output_path.display()))?;
96+
Ok(())
97+
}
98+
99+
fn required_path(value: Option<PathBuf>, name: &str) -> Result<PathBuf> {
100+
value.with_context(|| format!("{name} is required"))
101+
}
102+
103+
fn ensure_parent_dir(path: &Path) -> Result<()> {
104+
if let Some(parent) = path.parent()
105+
&& !parent.as_os_str().is_empty()
106+
{
107+
std::fs::create_dir_all(parent)
108+
.with_context(|| format!("failed to create {}", parent.display()))?;
109+
}
110+
Ok(())
111+
}
112+
113+
fn print_token_rates(output: &flint_ai::ScriptTextOutput) {
114+
if let (Some(tokens), Some(elapsed)) = (output.generated_tokens, output.elapsed) {
115+
let seconds = elapsed.as_secs_f64();
116+
if tokens > 0 && seconds > 0.0 {
117+
if output.decode_tokens.is_some() && output.decode_elapsed.is_some() {
118+
println!("tokens/s total: {:.2}", tokens as f64 / seconds);
119+
} else {
120+
println!("tokens/s: {:.2}", tokens as f64 / seconds);
121+
}
122+
}
123+
}
124+
if let (Some(tokens), Some(elapsed)) = (output.decode_tokens, output.decode_elapsed) {
125+
let seconds = elapsed.as_secs_f64();
126+
if tokens > 0 && seconds > 0.0 {
127+
println!("tokens/s decode: {:.2}", tokens as f64 / seconds);
128+
}
129+
}
130+
}

src/lib.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ use anyhow::{Context, Result, anyhow, ensure};
88
use image::{DynamicImage, GrayImage, RgbImage};
99
use imageproc::contours::{BorderType, find_contours_with_threshold};
1010
use koharu_runtime::package::{Package, libtorch::Libtorch, loading::preload};
11-
use koharu_torch::{Device, Kind, Tensor};
11+
use koharu_torch::{Cuda, Device, Kind, Tensor};
1212
pub(crate) use vm::{CallOutcome, Value, VmResult};
1313
use vm::{Program, compile_source};
1414

1515
use crate::host::TorchHostRuntime;
16-
pub use crate::host::TorchScriptRunner;
16+
pub use crate::host::{ScriptTextOutput, TorchScriptRunner};
1717

1818
pub struct LamaRustScript {
1919
device: Device,
@@ -185,6 +185,21 @@ pub fn parse_device(value: &str) -> Result<Device> {
185185
}
186186
}
187187

188+
pub fn auto_device() -> Device {
189+
if Cuda::is_available() && Cuda::device_count() > 0 {
190+
Device::Cuda(0)
191+
} else {
192+
Device::Cpu
193+
}
194+
}
195+
196+
pub fn resolve_device(value: Option<&str>) -> Result<Device> {
197+
match value {
198+
Some(value) => parse_device(value),
199+
None => Ok(auto_device()),
200+
}
201+
}
202+
188203
fn boxes_from_mask(mask: &GrayImage) -> Vec<[u32; 4]> {
189204
let width = mask.width();
190205
let mut left = width;

src/main.rs

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)