Compare commits
124 Commits
agentic-co
...
v0.2.4
| Author | SHA1 | Date | |
|---|---|---|---|
| afc7f9bcee | |||
| 5d2027b2ca | |||
| 8a4d515eed | |||
| 987a370a05 | |||
| f75e7f07e9 | |||
| eb6f720fcc | |||
| e25d0ea8f2 | |||
| 4d1e89da34 | |||
| bf535b6256 | |||
| 29e1c440c6 | |||
| 560bee1369 | |||
| b074e0cb49 | |||
| 9307c75516 | |||
| 86191fbb6c | |||
| a6a94f7688 | |||
| 8d5c5440d2 | |||
| a12bd7ce7f | |||
| 9ac90aa540 | |||
| 32065d5818 | |||
| 321943ee3c | |||
| 1b75c89320 | |||
| 01622a960f | |||
| 4e4efda67d | |||
| f5db2eb034 | |||
| 77c8d46e7b | |||
| f14eba1b49 | |||
| 6d15298418 | |||
| cea1961183 | |||
| 21a8015ea3 | |||
| c3991193d9 | |||
| 02c6d67218 | |||
| de1cf009fa | |||
| 060f36f479 | |||
| e2ec0fa43d | |||
| 8752c0f465 | |||
| 8c95282654 | |||
| a1bc1af646 | |||
| 6b27cbbade | |||
| 4d9c51a86f | |||
| 66d1e8c4b1 | |||
| 2eeac255f6 | |||
| 6097cfc263 | |||
| 8aed9f97a2 | |||
| c0ccd76a4c | |||
| d2edb38879 | |||
| 2755794554 | |||
| dbb37b3c60 | |||
| 0e7497b627 | |||
| 6b756e2e83 | |||
| 5a52f5113c | |||
| 7b0660e46e | |||
| b35600b417 | |||
| 7693269e5d | |||
| 702c9170ad | |||
| 3feed22055 | |||
| 75310c989e | |||
| 743946a391 | |||
| 0bd5faa684 | |||
| e0c8c3586b | |||
| 3a1c5c723c | |||
| 3139d1ac65 | |||
| 49a1629646 | |||
| 13008ac693 | |||
| 30e81875db | |||
| 73bcd3143a | |||
| 216b95d15c | |||
| 34ef19472a | |||
| 54a5af96c7 | |||
| 842153a7ec | |||
| 5c25c7f9c1 | |||
| ac698a766e | |||
| f1b57a6c53 | |||
| b70cdbd24d | |||
| 01d8b597e1 | |||
| f2ca4890df | |||
| 3eb0c4d939 | |||
| d8443792a3 | |||
| ae379bdda4 | |||
| ed02e47158 | |||
| 959dc532bb | |||
| 1ef7f7c956 | |||
| e6e1f60935 | |||
| 322c98ff59 | |||
| 406e2226f0 | |||
| 9d7496157c | |||
| d332b7e910 | |||
| 8e55a15d66 | |||
| 4e3134d908 | |||
| cd45db001a | |||
| 4ad8a8793e | |||
| b2694c232e | |||
| ba58236c52 | |||
| 861f2a6902 | |||
| 11fd5b0c9e | |||
| b3646ae5d3 | |||
| fc95cf8c1b | |||
| 1ae1bf98e2 | |||
| f567fd3f8a | |||
| 38367eac97 | |||
| 20716186bc | |||
| 4e810ed4a2 | |||
| 91ff9e00f9 | |||
| e652bf7ab6 | |||
| eb69893124 | |||
| d18314bfc8 | |||
| 99b011e399 | |||
|
|
3976bb6251 | ||
|
|
0c32fecdc4 | ||
|
|
801cc0371d | ||
|
|
176f2d6915 | ||
|
|
dd1945ab28 | ||
|
|
262fee3b49 | ||
|
|
aa7540a6bf | ||
|
|
762066102a | ||
|
|
bef5b6fc3c | ||
|
|
095b72d2d6 | ||
|
|
4cb6128a27 | ||
|
|
4dff534fbf | ||
|
|
d5ab6272d3 | ||
|
|
2e7b86deeb | ||
|
|
a6e49870d6 | ||
|
|
d68882249e | ||
|
|
6a587cd080 | ||
|
|
f17fcf0f9d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,3 +34,6 @@ Cargo.lock
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust,linux
|
||||
|
||||
# Ajonaikaiset tietokannat
|
||||
*.db
|
||||
|
||||
475
docker-errors.log
Normal file
475
docker-errors.log
Normal file
@@ -0,0 +1,475 @@
|
||||
[INFO]: Checking for the Wasm target...
|
||||
info: downloading component rust-std
|
||||
[INFO]: Compiling to Wasm...
|
||||
Compiling node v0.1.0 (/app/node)
|
||||
warning: unused imports: `DType`, `Device`, and `Tensor`
|
||||
--> node/src/smollm.rs:1:19
|
||||
|
|
||||
1 | use candle_core::{Device, Tensor, DType};
|
||||
| ^^^^^^ ^^^^^^ ^^^^^
|
||||
|
|
||||
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
|
||||
|
||||
warning: unused import: `candle_nn::VarBuilder`
|
||||
--> node/src/smollm.rs:2:5
|
||||
|
|
||||
2 | use candle_nn::VarBuilder;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
warning: unused imports: `Cache`, `LlamaConfig`, `LlamaEosToks`, and `Llama`
|
||||
--> node/src/smollm.rs:3:42
|
||||
|
|
||||
3 | use candle_transformers::models::llama::{Llama, LlamaConfig, LlamaEosToks, Cache};
|
||||
| ^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^
|
||||
|
||||
warning: unused imports: `DType`, `Device`, and `Tensor`
|
||||
--> node/src/phi3.rs:1:19
|
||||
|
|
||||
1 | use candle_core::{Device, Tensor, DType};
|
||||
| ^^^^^^ ^^^^^^ ^^^^^
|
||||
|
||||
warning: unused import: `candle_nn::VarBuilder`
|
||||
--> node/src/phi3.rs:2:5
|
||||
|
|
||||
2 | use candle_nn::VarBuilder;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
warning: unused imports: `Config as Phi3Config` and `Model as Phi3Model`
|
||||
--> node/src/phi3.rs:3:41
|
||||
|
|
||||
3 | use candle_transformers::models::phi3::{Config as Phi3Config, Model as Phi3Model};
|
||||
| ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
|
||||
|
||||
warning: unused import: `wasm_bindgen::JsCast`
|
||||
--> node/src/phi3.rs:4:5
|
||||
|
|
||||
4 | use wasm_bindgen::JsCast;
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
warning: unused import: `crate::storage`
|
||||
--> node/src/phi3.rs:9:5
|
||||
|
|
||||
9 | use crate::storage;
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
||||
warning: unused import: `Int`
|
||||
--> node/src/burn_smollm/attention.rs:2:46
|
||||
|
|
||||
2 | use burn::tensor::{backend::Backend, Tensor, Int};
|
||||
| ^^^
|
||||
|
||||
warning: unused imports: `Mlp` and `RmsNorm`
|
||||
--> node/src/burn_smollm/attention.rs:4:22
|
||||
|
|
||||
4 | use super::modules::{RmsNorm, Mlp};
|
||||
| ^^^^^^^ ^^^
|
||||
|
||||
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
|
||||
--> node/src/smollm.rs:174:23
|
||||
|
|
||||
174 | burn::tensor::Data::new(input_ids.iter().map(|&x| x as i32).collect::<Vec<_>>(), [input_len].into()),
|
||||
| ^^^^
|
||||
|
|
||||
= note: `#[warn(deprecated)]` on by default
|
||||
|
||||
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
|
||||
--> node/src/smollm.rs:200:27
|
||||
|
|
||||
200 | burn::tensor::Data::new(vec![next_token as i32], [1].into()),
|
||||
| ^^^^
|
||||
|
||||
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
|
||||
--> node/src/burn_smollm/loader.rs:1:46
|
||||
|
|
||||
1 | use burn::tensor::{backend::Backend, Tensor, Data};
|
||||
| ^^^^
|
||||
|
||||
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
|
||||
--> node/src/burn_smollm/loader.rs:17:16
|
||||
|
|
||||
17 | let data = Data::new(vec, shape_out_in.into());
|
||||
| ^^^^
|
||||
|
||||
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
|
||||
--> node/src/burn_smollm/loader.rs:32:16
|
||||
|
|
||||
32 | let data = Data::new(vec, shape.into());
|
||||
| ^^^^
|
||||
|
||||
warning: use of deprecated struct `burn::tensor::Data`: the internal data format has changed, please use `TensorData` instead
|
||||
--> node/src/burn_smollm/loader.rs:45:16
|
||||
|
|
||||
45 | let data = Data::new(vec, shape.into());
|
||||
| ^^^^
|
||||
|
||||
error[E0061]: this function takes 2 arguments but 1 argument was supplied
|
||||
--> node/src/smollm.rs:124:9
|
||||
|
|
||||
124 | burn_wgpu::init_async::<burn_wgpu::AutoGraphicsApi>(&Default::default()).await;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^--------------------- argument #2 of type `RuntimeOptions` is missing
|
||||
|
|
||||
note: function defined here
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cubecl-wgpu-0.2.0/src/runtime.rs:116:14
|
||||
|
|
||||
116 | pub async fn init_async<G: GraphicsApi>(device: &WgpuDevice, options: RuntimeOptions) {
|
||||
| ^^^^^^^^^^
|
||||
help: provide the argument
|
||||
|
|
||||
124 | burn_wgpu::init_async::<burn_wgpu::AutoGraphicsApi>(&Default::default(), /* RuntimeOptions */).await;
|
||||
| ++++++++++++++++++++++
|
||||
|
||||
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<i32, 1>>` is not satisfied
|
||||
--> node/src/smollm.rs:174:9
|
||||
|
|
||||
173 | let mut input_tensor = burn::tensor::Tensor::<B, 1, burn::tensor::Int>::from_data(
|
||||
| ---------------------------------------------------------- required by a bound introduced by this call
|
||||
174 | burn::tensor::Data::new(input_ids.iter().map(|&x| x as i32).collect::<Vec<_>>(), [input_len].into()),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<burn::tensor::Data<i32, 1>>` is not implemented for `TensorData`
|
||||
|
|
||||
= help: the following other types implement trait `From<T>`:
|
||||
`TensorData` implements `From<&[E]>`
|
||||
`TensorData` implements `From<&[usize]>`
|
||||
`TensorData` implements `From<[E; A]>`
|
||||
`TensorData` implements `From<[[E; B]; A]>`
|
||||
`TensorData` implements `From<[[[E; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[usize; A]>`
|
||||
= note: required for `burn::tensor::Data<i32, 1>` to implement `Into<TensorData>`
|
||||
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
||||
|
|
||||
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
|
||||
| --------- required by a bound in this associated function
|
||||
718 | where
|
||||
719 | T: Into<TensorData>,
|
||||
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
|
||||
|
||||
error[E0061]: this method takes 2 arguments but 0 arguments were supplied
|
||||
--> node/src/smollm.rs:183:51
|
||||
|
|
||||
183 | let next_token_tensor = last_logits.argmax(2).flatten::<1>();
|
||||
| ^^^^^^^^^^^^-- two arguments of type `usize` and `usize` are missing
|
||||
|
|
||||
note: method defined here
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:292:12
|
||||
|
|
||||
292 | pub fn flatten<const D2: usize>(self, start_dim: usize, end_dim: usize) -> Tensor<B, D2, K> {
|
||||
| ^^^^^^^
|
||||
help: provide the arguments
|
||||
|
|
||||
183 | let next_token_tensor = last_logits.argmax(2).flatten::<1>(/* usize */, /* usize */);
|
||||
| ++++++++++++++++++++++++
|
||||
|
||||
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<i32, 1>>` is not satisfied
|
||||
--> node/src/smollm.rs:200:13
|
||||
|
|
||||
199 | let mut input_tensor = burn::tensor::Tensor::<B, 1, burn::tensor::Int>::from_data(
|
||||
| ---------------------------------------------------------- required by a bound introduced by this call
|
||||
200 | burn::tensor::Data::new(vec![next_token as i32], [1].into()),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<burn::tensor::Data<i32, 1>>` is not implemented for `TensorData`
|
||||
|
|
||||
= help: the following other types implement trait `From<T>`:
|
||||
`TensorData` implements `From<&[E]>`
|
||||
`TensorData` implements `From<&[usize]>`
|
||||
`TensorData` implements `From<[E; A]>`
|
||||
`TensorData` implements `From<[[E; B]; A]>`
|
||||
`TensorData` implements `From<[[[E; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[usize; A]>`
|
||||
= note: required for `burn::tensor::Data<i32, 1>` to implement `Into<TensorData>`
|
||||
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
||||
|
|
||||
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
|
||||
| --------- required by a bound in this associated function
|
||||
718 | where
|
||||
719 | T: Into<TensorData>,
|
||||
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
|
||||
|
||||
error[E0061]: this method takes 2 arguments but 0 arguments were supplied
|
||||
--> node/src/smollm.rs:207:50
|
||||
|
|
||||
207 | let next_token_tensor = logits.argmax(2).flatten::<1>();
|
||||
| ^^^^^^^^^^^^-- two arguments of type `usize` and `usize` are missing
|
||||
|
|
||||
note: method defined here
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:292:12
|
||||
|
|
||||
292 | pub fn flatten<const D2: usize>(self, start_dim: usize, end_dim: usize) -> Tensor<B, D2, K> {
|
||||
| ^^^^^^^
|
||||
help: provide the arguments
|
||||
|
|
||||
207 | let next_token_tensor = logits.argmax(2).flatten::<1>(/* usize */, /* usize */);
|
||||
| ++++++++++++++++++++++++
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:58:13
|
||||
|
|
||||
58 | q = q.reshape([batch, seq_len, self.num_heads, self.head_dim]).swap_dims(1, 2);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 3>`
|
||||
found struct `burn::tensor::Tensor<_, 4>`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:59:13
|
||||
|
|
||||
59 | k = k.reshape([batch, seq_len, self.num_kv_heads, self.head_dim]).swap_dims(1, 2);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 3>`
|
||||
found struct `burn::tensor::Tensor<_, 4>`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:60:13
|
||||
|
|
||||
60 | v = v.reshape([batch, seq_len, self.num_kv_heads, self.head_dim]).swap_dims(1, 2);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 3>`
|
||||
found struct `burn::tensor::Tensor<_, 4>`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:63:31
|
||||
|
|
||||
63 | q = self.rope.forward(q, offset);
|
||||
| ------- ^ expected `4`, found `3`
|
||||
| |
|
||||
| arguments to this method are incorrect
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 4>`
|
||||
found struct `burn::tensor::Tensor<_, 3>`
|
||||
note: method defined here
|
||||
--> node/src/burn_smollm/rope.rs:35:12
|
||||
|
|
||||
35 | pub fn forward(&self, x: Tensor<B, 4>, offset: usize) -> Tensor<B, 4> {
|
||||
| ^^^^^^^ ---------------
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:63:13
|
||||
|
|
||||
63 | q = self.rope.forward(q, offset);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 3>`
|
||||
found struct `burn::tensor::Tensor<_, 4>`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:64:31
|
||||
|
|
||||
64 | k = self.rope.forward(k, offset);
|
||||
| ------- ^ expected `4`, found `3`
|
||||
| |
|
||||
| arguments to this method are incorrect
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 4>`
|
||||
found struct `burn::tensor::Tensor<_, 3>`
|
||||
note: method defined here
|
||||
--> node/src/burn_smollm/rope.rs:35:12
|
||||
|
|
||||
35 | pub fn forward(&self, x: Tensor<B, 4>, offset: usize) -> Tensor<B, 4> {
|
||||
| ^^^^^^^ ---------------
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:64:13
|
||||
|
|
||||
64 | k = self.rope.forward(k, offset);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `3`, found `4`
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 3>`
|
||||
found struct `burn::tensor::Tensor<_, 4>`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:68:41
|
||||
|
|
||||
68 | c.k = Tensor::cat(vec![c.k, k], 2);
|
||||
| ^ expected `4`, found `3`
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 4>`
|
||||
found struct `burn::tensor::Tensor<_, 3>`
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> node/src/burn_smollm/attention.rs:69:41
|
||||
|
|
||||
69 | c.v = Tensor::cat(vec![c.v, v], 2);
|
||||
| ^ expected `4`, found `3`
|
||||
|
|
||||
= note: expected struct `burn::tensor::Tensor<_, 4>`
|
||||
found struct `burn::tensor::Tensor<_, 3>`
|
||||
|
||||
error[E0308]: `if` and `else` have incompatible types
|
||||
--> node/src/burn_smollm/attention.rs:72:13
|
||||
|
|
||||
67 | let (k, v) = if let Some(mut c) = cache {
|
||||
| ______________________-
|
||||
68 | | c.k = Tensor::cat(vec![c.k, k], 2);
|
||||
69 | | c.v = Tensor::cat(vec![c.v, v], 2);
|
||||
70 | | (c.k.clone(), c.v.clone())
|
||||
| | -------------------------- expected because of this
|
||||
71 | | } else {
|
||||
72 | | (k.clone(), v.clone())
|
||||
| | ^^^^^^^^^^^^^^^^^^^^^^ expected `4`, found `3`
|
||||
73 | | };
|
||||
| |_________- `if` and `else` have incompatible types
|
||||
|
|
||||
= note: expected tuple `(burn::tensor::Tensor<_, 4>, burn::tensor::Tensor<_, 4>)`
|
||||
found tuple `(burn::tensor::Tensor<_, 3>, burn::tensor::Tensor<_, 3>)`
|
||||
|
||||
error[E0282]: type annotations needed
|
||||
--> node/src/burn_smollm/attention.rs:75:38
|
||||
|
|
||||
75 | let new_cache = KVCache { k: k.clone(), v: v.clone() };
|
||||
| ^ cannot infer type
|
||||
|
||||
error[E0282]: type annotations needed
|
||||
--> node/src/burn_smollm/attention.rs:75:52
|
||||
|
|
||||
75 | let new_cache = KVCache { k: k.clone(), v: v.clone() };
|
||||
| ^ cannot infer type
|
||||
|
||||
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<f32, 2>>` is not satisfied
|
||||
--> node/src/burn_smollm/loader.rs:18:44
|
||||
|
|
||||
18 | let t_burn = Tensor::<B, 2>::from_data(data, device);
|
||||
| ------------------------- ^^^^ the trait `From<burn::tensor::Data<f32, 2>>` is not implemented for `TensorData`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the following other types implement trait `From<T>`:
|
||||
`TensorData` implements `From<&[E]>`
|
||||
`TensorData` implements `From<&[usize]>`
|
||||
`TensorData` implements `From<[E; A]>`
|
||||
`TensorData` implements `From<[[E; B]; A]>`
|
||||
`TensorData` implements `From<[[[E; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[usize; A]>`
|
||||
= note: required for `burn::tensor::Data<f32, 2>` to implement `Into<TensorData>`
|
||||
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
||||
|
|
||||
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
|
||||
| --------- required by a bound in this associated function
|
||||
718 | where
|
||||
719 | T: Into<TensorData>,
|
||||
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
|
||||
|
||||
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<f32, 1>>` is not satisfied
|
||||
--> node/src/burn_smollm/loader.rs:33:53
|
||||
|
|
||||
33 | Ok(Param::from_tensor(Tensor::<B, 1>::from_data(data, device)))
|
||||
| ------------------------- ^^^^ the trait `From<burn::tensor::Data<f32, 1>>` is not implemented for `TensorData`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the following other types implement trait `From<T>`:
|
||||
`TensorData` implements `From<&[E]>`
|
||||
`TensorData` implements `From<&[usize]>`
|
||||
`TensorData` implements `From<[E; A]>`
|
||||
`TensorData` implements `From<[[E; B]; A]>`
|
||||
`TensorData` implements `From<[[[E; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[usize; A]>`
|
||||
= note: required for `burn::tensor::Data<f32, 1>` to implement `Into<TensorData>`
|
||||
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
||||
|
|
||||
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
|
||||
| --------- required by a bound in this associated function
|
||||
718 | where
|
||||
719 | T: Into<TensorData>,
|
||||
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
|
||||
|
||||
error[E0277]: the trait bound `TensorData: From<burn::tensor::Data<f32, 2>>` is not satisfied
|
||||
--> node/src/burn_smollm/loader.rs:47:53
|
||||
|
|
||||
47 | Ok(Param::from_tensor(Tensor::<B, 2>::from_data(data, device)))
|
||||
| ------------------------- ^^^^ the trait `From<burn::tensor::Data<f32, 2>>` is not implemented for `TensorData`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the following other types implement trait `From<T>`:
|
||||
`TensorData` implements `From<&[E]>`
|
||||
`TensorData` implements `From<&[usize]>`
|
||||
`TensorData` implements `From<[E; A]>`
|
||||
`TensorData` implements `From<[[E; B]; A]>`
|
||||
`TensorData` implements `From<[[[E; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[E; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[[[[[Elem; E]; D]; C]; B]; A]>`
|
||||
`TensorData` implements `From<[usize; A]>`
|
||||
= note: required for `burn::tensor::Data<f32, 2>` to implement `Into<TensorData>`
|
||||
note: required by a bound in `burn::tensor::Tensor::<B, D, K>::from_data`
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:719:12
|
||||
|
|
||||
717 | pub fn from_data<T>(data: T, device: &B::Device) -> Self
|
||||
| --------- required by a bound in this associated function
|
||||
718 | where
|
||||
719 | T: Into<TensorData>,
|
||||
| ^^^^^^^^^^^^^^^^ required by this bound in `Tensor::<B, D, K>::from_data`
|
||||
|
||||
error[E0599]: no function or associated item named `arange` found for struct `burn::tensor::Tensor<B, 1>` in the current scope
|
||||
--> node/src/burn_smollm/rope.rs:19:33
|
||||
|
|
||||
19 | let t = Tensor::<B, 1>::arange(0..max_seq_len as i64, device).float().unsqueeze::<2>().transpose();
|
||||
| ^^^^^^ function or associated item not found in `burn::tensor::Tensor<B, 1>`
|
||||
|
|
||||
note: if you're trying to build a new `burn::tensor::Tensor<B, 1>` consider using one of the following associated functions:
|
||||
burn::tensor::Tensor::<B, D, K>::new
|
||||
burn::tensor::Tensor::<B, D, K>::from_primitive
|
||||
burn::tensor::Tensor::<B, D, K>::empty
|
||||
burn::tensor::Tensor::<B, D, K>::from_data
|
||||
and 9 others
|
||||
--> /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.14.0/src/tensor/api/base.rs:24:10
|
||||
|
|
||||
24 | #[derive(new, Clone, Debug)]
|
||||
| ^^^
|
||||
...
|
||||
55 | pub fn from_primitive(tensor: K::Primitive<D>) -> Self {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
...
|
||||
60 | pub fn empty<S: Into<Shape<D>>>(shape: S, device: &B::Device) -> Self {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
...
|
||||
717 | / pub fn from_data<T>(data: T, device: &B::Device) -> Self
|
||||
718 | | where
|
||||
719 | | T: Into<TensorData>,
|
||||
| |____________________________^
|
||||
= note: the function or associated item was found for
|
||||
- `burn::tensor::Tensor<B, 1, burn::tensor::Int>`
|
||||
= note: this error originates in the derive macro `new` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
warning: variable does not need to be mutable
|
||||
--> node/src/burn_smollm/loader.rs:70:13
|
||||
|
|
||||
70 | let mut layer = &mut model.layers[i];
|
||||
| ----^^^^^
|
||||
| |
|
||||
| help: remove this `mut`
|
||||
|
|
||||
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
|
||||
|
||||
warning: unused variable: `batch`
|
||||
--> node/src/burn_smollm/model.rs:79:14
|
||||
|
|
||||
79 | let [batch, seq_len] = input_ids.dims();
|
||||
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_batch`
|
||||
|
|
||||
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
|
||||
|
||||
warning: unused variable: `seq_len`
|
||||
--> node/src/burn_smollm/model.rs:79:21
|
||||
|
|
||||
79 | let [batch, seq_len] = input_ids.dims();
|
||||
| ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_seq_len`
|
||||
|
||||
Some errors have detailed explanations: E0061, E0277, E0282, E0308, E0599.
|
||||
For more information about an error, try `rustc --explain E0061`.
|
||||
warning: `node` (lib) generated 19 warnings
|
||||
error: could not compile `node` (lib) due to 21 previous errors; 19 warnings emitted
|
||||
Error: Compiling your crate to WebAssembly failed
|
||||
Caused by: Compiling your crate to WebAssembly failed
|
||||
Caused by: failed to execute `cargo build`: exited with exit status: 101
|
||||
full command: cd "/app/node" && "cargo" "build" "--lib" "--release" "--target" "wasm32-unknown-unknown"
|
||||
@@ -1,30 +1,36 @@
|
||||
FROM rust:slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
pkg-config libssl-dev g++ \
|
||||
pkg-config libssl-dev g++ libvulkan-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY Cargo.toml ./
|
||||
COPY Cargo.loc[k] ./
|
||||
COPY hub/Cargo.toml hub/Cargo.toml
|
||||
COPY node/Cargo.toml node/Cargo.toml
|
||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||
COPY cli/Cargo.toml cli/Cargo.toml
|
||||
|
||||
# Tyhjät src-tiedostot riippuvuuksien esikääntämistä varten
|
||||
RUN mkdir -p hub/src node/src native-node/src \
|
||||
RUN mkdir -p hub/src node/src native-node/src cli/src \
|
||||
&& echo "fn main(){}" > hub/src/main.rs \
|
||||
&& echo "" > node/src/lib.rs \
|
||||
&& echo "fn main(){}" > native-node/src/main.rs \
|
||||
&& echo "fn main(){}" > cli/src/main.rs \
|
||||
&& cargo build --release -p native-node 2>/dev/null || true
|
||||
|
||||
COPY native-node/src native-node/src
|
||||
RUN cargo build --release -p native-node
|
||||
# Touch pakottaa rekompilauksen dummy-binaryn yli
|
||||
RUN touch native-node/src/main.rs && cargo build --release -p native-node
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y ca-certificates libvulkan1 && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/target/release/native-node /usr/local/bin/native-node
|
||||
|
||||
ENV HUB_URL=ws://hub:3000/ws
|
||||
ENV HUB_URL=ws://agentic-poc:3000/ws
|
||||
ENV OLLAMA_URL=http://ollama:11434
|
||||
ENV OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
ENV ALLOCATED_GB=4
|
||||
|
||||
CMD ["native-node"]
|
||||
|
||||
348
network-poc/PROMPTS.md
Normal file
348
network-poc/PROMPTS.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Kipinä Agentic Studio — Promptit
|
||||
|
||||
Kaikki järjestelmässä käytetyt promptit. Jokainen on dokumentoitu eksaktisti
|
||||
niin kuin se lähetetään mallille, muuttujat merkitty `${...}`-syntaksilla.
|
||||
|
||||
---
|
||||
|
||||
## 1. Inferenssin system prompt (Wasm + natiivi)
|
||||
|
||||
**Sijainti:** `node/src/qwen_coder.rs` rivi 256, `native-node/src/inference.rs` rivi 127
|
||||
**Malli:** Qwen2.5-Coder-0.5B/3B
|
||||
**ChatML-rooli:** `<|im_start|>system`
|
||||
|
||||
```
|
||||
You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.
|
||||
```
|
||||
|
||||
**Tarkoitus:** Pakottaa malli tuottamaan pelkkää koodia ilman selityksiä.
|
||||
**Prefill:** Assistantin vastaus alkaa ` ``` ` joka ohjaa mallin koodiblokkiin.
|
||||
|
||||
---
|
||||
|
||||
## 2. Agenttikohtaiset system promptit (frontend)
|
||||
|
||||
**Sijainti:** `static/index.html` rivit 1136-1144
|
||||
**Tallennus:** localStorage (`kpn-agent-prompt-{key}`)
|
||||
**ChatML-rooli:** Liitetään `<|im_start|>user` -blokkiin osaksi promptia
|
||||
|
||||
### 2.1 Manageri (manager)
|
||||
```
|
||||
Olet projektipäällikkö. Jaa tehtävät osiin, priorisoi ja koordinoi tiimin työtä.
|
||||
```
|
||||
**Malli:** qwen-coder
|
||||
|
||||
### 2.2 Koodari (coder)
|
||||
```
|
||||
Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.
|
||||
```
|
||||
**Malli:** qwen-coder
|
||||
|
||||
### 2.3 Data-agentti (data)
|
||||
```
|
||||
Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.
|
||||
```
|
||||
**Malli:** qwen-coder
|
||||
|
||||
### 2.4 QA (qa)
|
||||
```
|
||||
Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.
|
||||
```
|
||||
**Malli:** smollm-135m
|
||||
|
||||
### 2.5 DevOps / Testaaja (tester)
|
||||
```
|
||||
Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.
|
||||
```
|
||||
**Malli:** smollm-135m
|
||||
|
||||
### 2.6 Tarkkailija (observer)
|
||||
```
|
||||
Olet ohjelmistoprojektin riippumaton valvoja. Sinulla on täysi pääsy kaikkiin projektin tietoihin ja muiden agenttien keskusteluihin. Valvo tiimin (Manageri, Koodari, Data, QA, DevOps) toimintaa asiantuntijana kokonaisuutena ja huomauta välittömästi visio- tai turvallisuusriskeistä.
|
||||
```
|
||||
**Malli:** deepseek-r1
|
||||
|
||||
### 2.7 Asiakas (client)
|
||||
```
|
||||
Kirjoita tähän asiakkaan toiveet ja projektin vaatimukset. Orkestraattori (Manageri) purkaa ja delegoi nämä työt asiantuntijoille.
|
||||
```
|
||||
**Malli:** user-input (ei LLM:ää, käyttäjän teksti)
|
||||
|
||||
---
|
||||
|
||||
## 3. Projekti-pipeline (`kpn project`)
|
||||
|
||||
### 3.1 Vaihe 1: Managerin tiedostojako
|
||||
|
||||
**Konteksti:** Käyttäjä on antanut projektin kuvauksen.
|
||||
**Tavoite:** Pilkotaan projekti yksittäisiksi tiedostoiksi oikeassa riippuvuusjärjestyksessä.
|
||||
|
||||
```
|
||||
List the source files needed for this project. One file per line, format:
|
||||
filename.py: what this file contains
|
||||
|
||||
Rules:
|
||||
- Max 4 files
|
||||
- Only .py, .toml, .json, .html files
|
||||
- No directories, no paths, just filenames
|
||||
- List dependencies first, then main app (e.g. models.py before main.py)
|
||||
- Use pyproject.toml for dependencies (not requirements.txt)
|
||||
|
||||
Project: ${task}
|
||||
```
|
||||
|
||||
**Odotettu vastausformaatti:**
|
||||
```
|
||||
models.py: SQLAlchemy User model and database setup
|
||||
main.py: FastAPI app with CRUD endpoints
|
||||
pyproject.toml: project dependencies
|
||||
```
|
||||
|
||||
**Parsintasäännöt:**
|
||||
- Rivi voi olla `filename.ext: kuvaus` tai pelkkä `filename.ext`
|
||||
- Tiedostonimessä ei saa olla `/`, välilyöntejä tai polkuja
|
||||
- Päättyy tiedostopäätteeseen (`/\.\w{1,5}$/`)
|
||||
- Numerot, `-`, `*` ja `` ` `` strippataan rivin alusta
|
||||
- Max 40 merkin tiedostonimi
|
||||
|
||||
### 3.2 Vaihe 2: Koodarin tiedostogenerointi (per tiedosto)
|
||||
|
||||
**Konteksti:** Managerin tiedostolista on parsittu. Jokaiselle tiedostolle generoidaan koodi erikseen. Aiemmin generoidut tiedostot annetaan kontekstina.
|
||||
|
||||
**Perusmuoto:**
|
||||
```
|
||||
${context}Project: ${task}
|
||||
Write ONLY the file "${filename}"${description ? ': ' + description : ''}.
|
||||
Use the exact libraries mentioned in the project description. Write correct, working code.
|
||||
```
|
||||
|
||||
**`${context}` (kun aiempia tiedostoja on generoitu):**
|
||||
```
|
||||
Already written files:
|
||||
--- models.py ---
|
||||
from sqlalchemy import ...
|
||||
...
|
||||
|
||||
--- main.py ---
|
||||
from fastapi import ...
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
**Erikoistapaus: pyproject.toml**
|
||||
|
||||
Koska 0.5B-malli ei tunne uv/pyproject.toml-formaattia, annetaan eksplisiittinen esimerkki:
|
||||
```
|
||||
${context}Project: ${task}
|
||||
Write ONLY the file "pyproject.toml": ${description}.
|
||||
Use this exact format:
|
||||
[project]
|
||||
name = "projectname"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["fastapi", "uvicorn"]
|
||||
|
||||
[project.scripts]
|
||||
start = "uvicorn main:app --reload"
|
||||
Use the exact libraries mentioned in the project description. Write correct, working code.
|
||||
```
|
||||
|
||||
**Erikoistapaus: requirements.txt (fallback)**
|
||||
```
|
||||
...
|
||||
List one dependency per line. No version pins unless necessary.
|
||||
...
|
||||
```
|
||||
|
||||
### 3.3 Vaihe 2 (fallback): Yhtenä kokonaisuutena
|
||||
|
||||
Jos managerin vastaus ei tuota parsittavaa tiedostolistaa:
|
||||
```
|
||||
Project: ${task}
|
||||
Files: ${managerin_vastaus}
|
||||
|
||||
Write all the code for this project. Use the exact libraries mentioned in the project description. Use pyproject.toml for dependencies (not requirements.txt).
|
||||
```
|
||||
|
||||
### 3.4 Vaihe 3: Testerin arviointi
|
||||
|
||||
**Konteksti:** Kaikki generoidut tiedostot yhdistettynä.
|
||||
|
||||
```
|
||||
Review this project. List bugs or issues. Be brief.
|
||||
If the code is correct, say "LGTM".
|
||||
|
||||
--- models.py ---
|
||||
from sqlalchemy import ...
|
||||
|
||||
--- main.py ---
|
||||
from fastapi import ...
|
||||
```
|
||||
|
||||
**Odotettu vastaus:** Bugilista tai `LGTM`.
|
||||
**Trigger korjausluuppiin:** Jos vastaus EI sisällä "lgtm" tai "looks good" (case-insensitive).
|
||||
|
||||
### 3.5 Vaihe 4: Koodarin korjaukset (ehdollinen)
|
||||
|
||||
Ajetaan vain jos testeri löysi ongelmia.
|
||||
|
||||
```
|
||||
Fix the issues found in the review.
|
||||
Review feedback: ${review}
|
||||
|
||||
Current code:
|
||||
--- models.py ---
|
||||
...
|
||||
|
||||
--- main.py ---
|
||||
...
|
||||
|
||||
Write the corrected code.
|
||||
```
|
||||
|
||||
### 3.6 Vaihe 5: Testerin uudelleenarviointi (ehdollinen)
|
||||
|
||||
```
|
||||
Review the corrected code briefly:
|
||||
${fixedCode}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Yksinkertainen pipeline (`kpn pipeline`)
|
||||
|
||||
### 4.1 Manageri
|
||||
```
|
||||
Analyse this task briefly and write a technical spec for a coder:
|
||||
${task}
|
||||
```
|
||||
|
||||
### 4.2 Koodari
|
||||
```
|
||||
${managerin_vastaus}
|
||||
|
||||
Write the code.
|
||||
```
|
||||
|
||||
### 4.3 Testaaja
|
||||
```
|
||||
Review briefly:
|
||||
${koodarin_vastaus}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Yksittäiset komennot
|
||||
|
||||
### 5.1 `kpn run <malli> "<prompti>"`
|
||||
|
||||
Promptin koostaminen `kpnRun`-funktiossa:
|
||||
```
|
||||
${sharedPrompt} ← Kaikille agenteille yhteinen (jos asetettu)
|
||||
|
||||
${agentPrompt} ← Valitun agentin system prompt (jos löytyy)
|
||||
|
||||
${käyttäjän_prompti} ← Käyttäjän kirjoittama teksti
|
||||
```
|
||||
|
||||
Osat yhdistetään `\n\n`-erottimella ja lähetetään `<|im_start|>user`-blokkiin.
|
||||
|
||||
### 5.2 `kpn hello`
|
||||
|
||||
Kiinteä prompti SmolLM-135M -mallille:
|
||||
```
|
||||
Tervehdi käyttäjää iloisesti ja lyhyesti suomeksi. Ole innostunut ja energinen! Vastaa yhdellä lauseella.
|
||||
```
|
||||
|
||||
### 5.3 Warmup (automaattinen)
|
||||
|
||||
Lähetetään automaattisesti kun laskentasolmu käynnistyy. Triggeröi mallin latauksen ilman näkyvää tulosta.
|
||||
```json
|
||||
{"prompt": "warmup", "max_tokens": 1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Stop-sekvenssit (inferenssi)
|
||||
|
||||
**Sijainti:** `node/src/qwen_coder.rs` rivi 345, `native-node/src/inference.rs` rivi 210
|
||||
|
||||
Generointi katkaistaan ja teksti trimmataan kun malli tuottaa minkä tahansa näistä:
|
||||
|
||||
| Sekvenssi | Tarkoitus |
|
||||
|-----------|-----------|
|
||||
| `\n###` | Markdown-otsikko (selitysosio alkaa) |
|
||||
| `\nExplanation` | Selitysosio |
|
||||
| `\nNote:` | Huomautus |
|
||||
| `\nOutput:` | Esimerkkitulostus |
|
||||
| `` \n```\n\n `` | Koodiblokin loppu + tyhjä rivi |
|
||||
| `\n// Example` | Esimerkkikoodi (C/Rust/JS) |
|
||||
| `\n// example` | Sama pienellä |
|
||||
| `\n# Example` | Esimerkkikoodi (Python/Ruby) |
|
||||
| `\n# example` | Sama pienellä |
|
||||
|
||||
---
|
||||
|
||||
## 7. Vastauksen siivous (post-processing)
|
||||
|
||||
**Sijainti:** `strip_markdown_wrapper()` molemmissa inferenssimoduuleissa
|
||||
|
||||
### 7.1 Kielitunnisteen poisto
|
||||
|
||||
Jos ensimmäinen rivi on tunnettu kielitunniste, se poistetaan.
|
||||
Tunnistetut: `python`, `py`, `rust`, `rs`, `javascript`, `js`, `typescript`, `ts`,
|
||||
`java`, `kotlin`, `scala`, `go`, `ruby`, `rb`, `php`, `swift`,
|
||||
`c`, `cpp`, `c++`, `c#`, `csharp`, `r`, `sql`, `bash`, `sh`, `zsh`,
|
||||
`html`, `css`, `json`, `yaml`, `yml`, `toml`, `xml`, `markdown`, `md`,
|
||||
`lua`, `perl`, `dart`, `elixir`, `haskell`, `hs`, `ocaml`, `zig`,
|
||||
`plaintext`, `text`, `txt`
|
||||
|
||||
### 7.2 Sulkevan ` ``` ` poisto
|
||||
|
||||
Poistetaan VAIN jos ` ``` ` on omalla rivillään tiedoston lopussa
|
||||
(edeltävä merkki on rivinvaihto tai alku).
|
||||
|
||||
### 7.3 Johdantolauseiden poisto
|
||||
|
||||
Ensimmäinen rivi poistetaan jos se alkaa (case-insensitive):
|
||||
`Sure!`, `Here is`, `Here's`, `Certainly!`, `Below is`
|
||||
|
||||
### 7.4 Selityskommenttien poisto
|
||||
|
||||
Alun `# `-alkuiset rivit poistetaan jos ne sisältävät (case-insensitive):
|
||||
`this is`, `simple`, `program that`, `here is`, `the following`, `below`
|
||||
|
||||
Shebang (`#!`) säilytetään.
|
||||
|
||||
---
|
||||
|
||||
## 8. Promptin kulku mallille (ChatML)
|
||||
|
||||
Lopullinen viesti mallille koostetaan näin:
|
||||
|
||||
```
|
||||
<|im_start|>system
|
||||
You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>
|
||||
<|im_start|>user
|
||||
${sharedPrompt}
|
||||
|
||||
${agentPrompt}
|
||||
|
||||
${käyttäjän/pipelinen prompti}<|im_end|>
|
||||
<|im_start|>assistant
|
||||
```
|
||||
```
|
||||
|
||||
**Huomio:** ` ``` ` assistantin alussa on prefill — se on osa syötettä eikä mallin tuottamaa. Malli jatkaa suoraan koodilla.
|
||||
|
||||
---
|
||||
|
||||
## 9. Sampling-parametrit
|
||||
|
||||
| Parametri | Arvo | Kuvaus |
|
||||
|-----------|------|--------|
|
||||
| `temperature` | 0.7 | Jakaumaa pehmentävä kerroin |
|
||||
| `top_k` | 40 | Valinnan rajoitus 40 todennäköisimpään tokeniin |
|
||||
| `repetition_penalty` | 1.15 | Aiemmin generoitujen tokenien rankaisu |
|
||||
| `max_tokens` | 512 (oletus) | Konfiguroitavissa JSON-promptilla |
|
||||
| `eos_token` | 151645 | Qwen2.5:n päätöstokeni |
|
||||
26
network-poc/TODO.md
Normal file
26
network-poc/TODO.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# TODO — Kipinä Agentic Network
|
||||
|
||||
## Turvallisuus
|
||||
- [ ] **Tulosten validointi** — solmu voi palauttaa haitallista koodia. Tarvitaan proof-of-work tai challenge-response -mekanismi
|
||||
- [ ] **Reputaatiojärjestelmä** — solmujen luotettavuuden seuranta: onnistuneet tehtävät, vasteaika, laatu
|
||||
- [ ] **Koodin sandboxaus** — generoitu koodi pitää ajaa eristetyssä ympäristössä ennen käyttäjälle näyttämistä
|
||||
- [ ] **Solmun identiteetti** — rekisteröityminen ja tunnistautuminen (API-avain / token)
|
||||
|
||||
## Yksityisyys
|
||||
- [ ] **Promptien salaus** — käyttäjän promptit menevät tuntemattomalle solmulle selkotekstinä
|
||||
- [ ] **End-to-end enkryptio** — hub ei näe promptin sisältöä, vain reitittää
|
||||
- [ ] **Tietosuojaseloste** — käyttäjille kerrottava miten data kulkee ja kuka sen näkee
|
||||
- [ ] **Opt-in malli** — käyttäjä valitsee haluaako käyttää yhteisösolmuja vai vain omaa
|
||||
|
||||
## Väärinkäytön esto
|
||||
- [ ] **Rate limiting per käyttäjä** — nykyinen IP-pohjainen ei riitä, tarvitaan autentikointi
|
||||
- [ ] **Solmun kuormitusraja** — solmu voi asettaa max tehtävät/minuutti
|
||||
- [ ] **Token-talous** — laskentaresurssien käyttö vaatii Kipinä-tokeneita (gamification jo aloitettu)
|
||||
- [ ] **Abuse reporting** — mekanismi haitallisten solmujen ilmiantamiseen
|
||||
|
||||
## Seuraavat ominaisuudet
|
||||
- [ ] Agenttien välinen keskustelu (manageri ohjaa dynaamisesti)
|
||||
- [ ] Tehtävähistoria ja tulosten tallennus
|
||||
- [ ] Prometheus/OpenTelemetry -metriikat
|
||||
- [ ] Solmujen terveystarkistukset (ping/pong)
|
||||
- [ ] Streaming-vastaukset Ollaman kautta
|
||||
@@ -1,13 +1,12 @@
|
||||
services:
|
||||
# NVIDIA GPU -solmu
|
||||
native-node-nvidia:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina-node-nvidia
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- ALLOCATED_GB=4
|
||||
# Ollama NVIDIA GPU:lla
|
||||
ollama-nvidia:
|
||||
image: ollama/ollama:latest
|
||||
container_name: kipina-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
@@ -16,6 +15,65 @@ services:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ollama
|
||||
profiles:
|
||||
- nvidia
|
||||
|
||||
# Ollama AMD ROCm GPU:lla
|
||||
ollama-amd:
|
||||
image: ollama/ollama:rocm
|
||||
container_name: kipina-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
restart: unless-stopped
|
||||
devices:
|
||||
- /dev/kfd:/dev/kfd
|
||||
- /dev/dri:/dev/dri
|
||||
group_add:
|
||||
- video
|
||||
- render
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ollama
|
||||
profiles:
|
||||
- amd
|
||||
|
||||
# Ollama CPU:lla
|
||||
ollama-cpu:
|
||||
image: ollama/ollama:latest
|
||||
container_name: kipina-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ollama
|
||||
profiles:
|
||||
- cpu
|
||||
|
||||
# NVIDIA GPU -solmu
|
||||
native-node-nvidia:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina-node-nvidia
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ollama-nvidia
|
||||
profiles:
|
||||
- nvidia
|
||||
|
||||
@@ -27,14 +85,12 @@ services:
|
||||
container_name: kipina-node-amd
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=4
|
||||
restart: unless-stopped
|
||||
devices:
|
||||
- /dev/kfd:/dev/kfd
|
||||
- /dev/dri:/dev/dri
|
||||
group_add:
|
||||
- video
|
||||
- render
|
||||
depends_on:
|
||||
- ollama-amd
|
||||
profiles:
|
||||
- amd
|
||||
|
||||
@@ -46,7 +102,14 @@ services:
|
||||
container_name: kipina-node-cpu
|
||||
environment:
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=2
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ollama-cpu
|
||||
profiles:
|
||||
- cpu
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
|
||||
@@ -9,26 +9,39 @@ services:
|
||||
volumes:
|
||||
- .:/app
|
||||
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
|
||||
command: bash -c "cd node && wasm-pack build --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
|
||||
|
||||
# Valinnainen natiivi-solmu — kerää oikeat laitteistotiedot (nvidia-smi-taso)
|
||||
# Ollama — LLM-inferenssi
|
||||
# NVIDIA: vaihda image → ollama/ollama:latest ja lisää deploy.resources (ks. README)
|
||||
# CPU: vaihda image → ollama/ollama:latest ja poista devices
|
||||
ollama:
|
||||
image: ollama/ollama:rocm
|
||||
container_name: kipina_ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
devices:
|
||||
- /dev/kfd
|
||||
- /dev/dri
|
||||
profiles:
|
||||
- native
|
||||
|
||||
# Natiivisolmu — yhdistää hubiin ja käyttää Ollamaa inferenssiin
|
||||
native-node:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.native-node
|
||||
container_name: kipina_native_node
|
||||
environment:
|
||||
- HUB_URL=ws://agentic-poc:3000/ws
|
||||
- HUB_URL=wss://kipina.studio/ws
|
||||
- OLLAMA_URL=http://ollama:11434
|
||||
- OLLAMA_MODEL=qwen2.5-coder:7b
|
||||
- ALLOCATED_GB=4
|
||||
depends_on:
|
||||
- agentic-poc
|
||||
# GPU passthrough (valinnainen — toimii myös ilman)
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
- ollama
|
||||
profiles:
|
||||
- native
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hub"
|
||||
version = "0.2.0"
|
||||
version = "0.2.4"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -16,3 +16,4 @@ futures = "0.3"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
chrono = "0.4"
|
||||
base64 = "0.22"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
|
||||
Binary file not shown.
@@ -39,6 +39,7 @@ struct AppState {
|
||||
ip_connections: Mutex<HashMap<IpAddr, u32>>,
|
||||
node_ips: Mutex<HashMap<u64, IpAddr>>,
|
||||
node_tasks: Mutex<HashMap<u64, String>>, // node_id → selected_task
|
||||
node_types: Mutex<HashMap<u64, String>>, // node_id → "native" | "browser"
|
||||
node_busy: Mutex<std::collections::HashSet<u64>>, // Solmut joilla on aktiivinen tehtävä
|
||||
pending_task_ids: Mutex<std::collections::HashSet<String>>, // Hubin jakamat task_id:t (gamification-validointi)
|
||||
api_rate_limits: Mutex<HashMap<IpAddr, (std::time::Instant, u32)>>, // IP → (ikkuna-alku, pyyntömäärä)
|
||||
@@ -260,6 +261,7 @@ async fn main() {
|
||||
ip_connections: Mutex::new(HashMap::new()),
|
||||
node_ips: Mutex::new(HashMap::new()),
|
||||
node_tasks: Mutex::new(HashMap::new()),
|
||||
node_types: Mutex::new(HashMap::new()),
|
||||
node_busy: Mutex::new(std::collections::HashSet::new()),
|
||||
pending_task_ids: Mutex::new(std::collections::HashSet::new()),
|
||||
api_rate_limits: Mutex::new(HashMap::new()),
|
||||
@@ -382,6 +384,9 @@ async fn main() {
|
||||
.route("/api/pairs", get(api_pairs))
|
||||
.route("/api/stats", get(api_stats))
|
||||
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
||||
.route("/api/v1/model", axum::routing::post(api_change_model))
|
||||
.route("/api/v1/hardware", get(api_hardware))
|
||||
.route("/api/v1/ollama/tags", get(api_ollama_tags))
|
||||
.route("/admin", get(admin_page))
|
||||
.nest_service("/", {
|
||||
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
|
||||
@@ -486,15 +491,15 @@ async fn ws_handler(
|
||||
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
||||
.unwrap_or_else(|| addr.ip());
|
||||
|
||||
// Max 2 yhteyttä per IP (dashboard-UI + selainsolmu)
|
||||
// Max yhteyttä per IP: jokainen selain tarvitsee 2 (UI + coder-node)
|
||||
{
|
||||
let conns = state.ip_connections.lock().unwrap();
|
||||
let count = conns.get(&ip).copied().unwrap_or(0);
|
||||
if count >= 4 {
|
||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/4) — estetty", ip, count);
|
||||
if count >= 10 {
|
||||
tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
|
||||
return (
|
||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||
"Max 4 yhteyttä per IP",
|
||||
"Max 10 yhteyttä per IP",
|
||||
).into_response();
|
||||
}
|
||||
}
|
||||
@@ -677,6 +682,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
||||
}
|
||||
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
||||
state.node_types.lock().unwrap().insert(node_id, node_type.to_string());
|
||||
|
||||
if node_type == "native" {
|
||||
let sys = json.get("system");
|
||||
@@ -934,6 +940,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
ips.remove(&node_id);
|
||||
vram.remove(&node_id);
|
||||
}
|
||||
state.node_types.lock().unwrap().remove(&node_id);
|
||||
tracing::info!("Solmu {} ({}) poistui verkosta.", node_id, ip);
|
||||
broadcast_stats(&state).await;
|
||||
sender_task.abort();
|
||||
@@ -943,6 +950,8 @@ struct ChatCompletionRequest {
|
||||
model: String,
|
||||
prompt: String,
|
||||
task_id: String,
|
||||
#[serde(default)]
|
||||
max_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -952,6 +961,78 @@ struct ChatCompletionResponse {
|
||||
tokens_generated: u64,
|
||||
}
|
||||
|
||||
async fn api_ollama_tags() -> axum::response::Response {
|
||||
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||
match reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
||||
Ok(resp) => {
|
||||
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||
axum::Json(body).into_response()
|
||||
} else {
|
||||
axum::Json(serde_json::json!({ "models": [] })).into_response()
|
||||
}
|
||||
}
|
||||
Err(_) => axum::Json(serde_json::json!({ "models": [] })).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_hardware(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
) -> axum::response::Response {
|
||||
// Etsitään natiivisolmun GPU-tiedot sessiosta
|
||||
let sessions = state.db.get_sessions(50);
|
||||
let native = sessions.iter().find(|s| {
|
||||
s.get("node_type").and_then(|v| v.as_str()) == Some("native")
|
||||
});
|
||||
|
||||
let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
|
||||
let gpus = s.get("gpus").and_then(|v| v.as_array());
|
||||
let gpu = gpus.and_then(|g| g.first());
|
||||
let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
(vram, name, ram)
|
||||
} else {
|
||||
(0, String::new(), 0)
|
||||
};
|
||||
|
||||
// Fallback: kysytään Ollamalta onko malleja ladattu (= Ollama on käynnissä)
|
||||
if vram_mb == 0 {
|
||||
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||
if let Ok(resp) = reqwest::get(format!("{}/api/tags", ollama_url)).await {
|
||||
if let Ok(body) = resp.json::<serde_json::Value>().await {
|
||||
let models = body["models"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||
if models > 0 {
|
||||
gpu_name = "Ollama (GPU/CPU)".to_string();
|
||||
// Natiivisolmun RAM fallbackina
|
||||
vram_mb = if ram_mb > 0 { ram_mb } else { 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gpu_name.is_empty() { gpu_name = "ei natiivisolmua".to_string(); }
|
||||
|
||||
axum::Json(serde_json::json!({
|
||||
"gpu_name": gpu_name,
|
||||
"vram_mb": vram_mb,
|
||||
"ram_mb": ram_mb,
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
async fn api_change_model(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
axum::Json(payload): axum::Json<serde_json::Value>,
|
||||
) -> axum::response::Response {
|
||||
let model = payload.get("model").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if model.is_empty() {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, "model puuttuu").into_response();
|
||||
}
|
||||
tracing::info!("Mallin vaihto: {}", model);
|
||||
let msg = serde_json::json!({ "type": "change_model", "model": model });
|
||||
let _ = state.stats_tx.send(msg.to_string());
|
||||
axum::Json(serde_json::json!({ "status": "ok", "model": model })).into_response()
|
||||
}
|
||||
|
||||
async fn api_chat_completions(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
@@ -966,24 +1047,30 @@ async fn api_chat_completions(
|
||||
*entry = (now, 1); // Uusi ikkuna
|
||||
} else {
|
||||
entry.1 += 1;
|
||||
if entry.1 > 10 {
|
||||
if entry.1 > 30 {
|
||||
return (axum::http::StatusCode::TOO_MANY_REQUESTS, "Liian monta pyyntöä — yritä minuutin kuluttua").into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Etsitään vapaa tai varattu solmu, joka vastaa pyydettyä mallia
|
||||
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
|
||||
let (target_node_free, target_node_any, total_matching) = {
|
||||
let tasks = state.node_tasks.lock().unwrap();
|
||||
let busy = state.node_busy.lock().unwrap();
|
||||
let node_types = state.node_types.lock().unwrap();
|
||||
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
|
||||
if payload.model == "qwen-coder" {
|
||||
*task == "qwen-coder-05b" || *task == "qwen-coder"
|
||||
task.starts_with("qwen-coder")
|
||||
} else {
|
||||
**task == payload.model
|
||||
}
|
||||
}).map(|(k, _)| *k).collect();
|
||||
let free = matching.iter().find(|id| !busy.contains(id)).copied();
|
||||
// Vapaat solmut: natiivi ensin, sitten selain
|
||||
let free_native = matching.iter().find(|id| {
|
||||
!busy.contains(id) && node_types.get(id).map(|t| t == "native").unwrap_or(false)
|
||||
}).copied();
|
||||
let free_any = matching.iter().find(|id| !busy.contains(id)).copied();
|
||||
let free = free_native.or(free_any);
|
||||
let any = matching.first().copied();
|
||||
(free, any, matching.len())
|
||||
};
|
||||
@@ -1059,12 +1146,15 @@ async fn api_chat_completions(
|
||||
state.node_busy.lock().unwrap().insert(target_node_id);
|
||||
state.pending_task_ids.lock().unwrap().insert(payload.task_id.clone());
|
||||
|
||||
let msg = serde_json::json!({
|
||||
let mut msg = serde_json::json!({
|
||||
"type": "llm_prompt",
|
||||
"prompt": payload.prompt,
|
||||
"model": payload.model,
|
||||
"task_id": payload.task_id,
|
||||
});
|
||||
if let Some(mt) = payload.max_tokens {
|
||||
msg.as_object_mut().unwrap().insert("max_tokens".to_string(), serde_json::json!(mt));
|
||||
}
|
||||
|
||||
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
|
||||
let mut rx = state.stats_tx.subscribe();
|
||||
@@ -1080,7 +1170,7 @@ async fn api_chat_completions(
|
||||
}
|
||||
}
|
||||
|
||||
let timeout = tokio::time::timeout(std::time::Duration::from_secs(120), async move {
|
||||
let timeout = tokio::time::timeout(std::time::Duration::from_secs(600), async move {
|
||||
loop {
|
||||
let msg_str = match rx.recv().await {
|
||||
Ok(msg) => msg,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "native-node"
|
||||
version = "0.1.0"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -12,10 +12,6 @@ serde_json = "1.0"
|
||||
sysinfo = "0.30"
|
||||
nvml-wrapper = "0.10"
|
||||
wgpu = "24"
|
||||
candle-core = { version = "0.8", features = ["cuda"] }
|
||||
candle-nn = "0.8"
|
||||
candle-transformers = "0.8"
|
||||
hf-hub = "0.4"
|
||||
tokenizers = "0.19"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
@@ -1,261 +1,144 @@
|
||||
use candle_core::{Device, Tensor, DType};
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
||||
use hf_hub::{api::sync::Api, Repo, RepoType};
|
||||
use std::time::Instant;
|
||||
|
||||
/// Top-k sampling with temperature and repetition penalty
|
||||
fn sample_top_k(logits: &Tensor, k: usize, temperature: f64, generated_tokens: &[u32], repetition_penalty: f64, rng_state: &mut u64) -> Result<u32, String> {
|
||||
let mut logits_vec: Vec<f32> = logits.to_vec1::<f32>().map_err(|e| format!("to_vec1: {}", e))?;
|
||||
if logits_vec.is_empty() { return Err("Tyhjä logits".to_string()); }
|
||||
|
||||
// Repetition penalty: rankaisee jo generoituja tokeneita
|
||||
for &token_id in generated_tokens {
|
||||
if (token_id as usize) < logits_vec.len() {
|
||||
let logit = &mut logits_vec[token_id as usize];
|
||||
if *logit > 0.0 {
|
||||
*logit /= repetition_penalty as f32;
|
||||
} else {
|
||||
*logit *= repetition_penalty as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature scaling
|
||||
if temperature > 0.0 && temperature != 1.0 {
|
||||
for logit in logits_vec.iter_mut() {
|
||||
*logit /= temperature as f32;
|
||||
}
|
||||
}
|
||||
|
||||
// Top-k: etsitään k suurinta
|
||||
let mut indexed: Vec<(usize, f32)> = logits_vec.iter().enumerate().map(|(i, &v)| (i, v)).collect();
|
||||
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
indexed.truncate(k);
|
||||
|
||||
if k == 1 || temperature == 0.0 {
|
||||
return Ok(indexed[0].0 as u32);
|
||||
}
|
||||
|
||||
// Softmax top-k:lle
|
||||
let max_logit = indexed[0].1;
|
||||
let exps: Vec<f32> = indexed.iter().map(|x| (x.1 - max_logit).exp()).collect();
|
||||
let sum: f32 = exps.iter().sum();
|
||||
let probs: Vec<f32> = exps.iter().map(|e| e / sum).collect();
|
||||
|
||||
// XorShift64 RNG
|
||||
*rng_state ^= *rng_state << 13;
|
||||
*rng_state ^= *rng_state >> 7;
|
||||
*rng_state ^= *rng_state << 17;
|
||||
let rand_val = (*rng_state % 10000) as f32 / 10000.0;
|
||||
|
||||
let mut cumulative = 0.0;
|
||||
for (i, p) in probs.iter().enumerate() {
|
||||
cumulative += p;
|
||||
if rand_val < cumulative {
|
||||
return Ok(indexed[i].0 as u32);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(indexed[0].0 as u32)
|
||||
}
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct LlmEngine {
|
||||
tokenizer: tokenizers::Tokenizer,
|
||||
model: QwenModel,
|
||||
device: Device,
|
||||
eos_token: u32,
|
||||
ollama_url: String,
|
||||
model: RefCell<String>,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl LlmEngine {
|
||||
pub fn load() -> Result<Self, String> {
|
||||
let device = Device::cuda_if_available(0).map_err(|e| format!("Device: {}", e))?;
|
||||
let device_name = if device.is_cuda() { "CUDA" } else { "CPU" };
|
||||
tracing::info!("LLM device: {}", device_name);
|
||||
pub async fn load() -> Result<Self, String> {
|
||||
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
|
||||
|
||||
let dtype = if device.is_cuda() { DType::F16 } else { DType::F32 };
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(3))
|
||||
.build()
|
||||
.map_err(|e| format!("HTTP client: {}", e))?;
|
||||
|
||||
tracing::info!("Ladataan Qwen2.5-Coder-0.5B-Instruct...");
|
||||
let api = Api::new().map_err(|e| format!("HF API: {}", e))?;
|
||||
let repo = api.repo(Repo::with_revision(
|
||||
"Qwen/Qwen2.5-Coder-0.5B-Instruct".to_string(),
|
||||
RepoType::Model,
|
||||
"main".to_string(),
|
||||
));
|
||||
|
||||
let tokenizer_path = repo.get("tokenizer.json").map_err(|e| format!("Tokenizer lataus: {}", e))?;
|
||||
let model_path = repo.get("model.safetensors").map_err(|e| format!("Malli lataus: {}", e))?;
|
||||
|
||||
tracing::info!("Ladataan tokenizer: {:?}", tokenizer_path);
|
||||
let tokenizer = tokenizers::Tokenizer::from_file(&tokenizer_path)
|
||||
.map_err(|e| format!("Tokenizer: {}", e))?;
|
||||
|
||||
let config = QwenConfig {
|
||||
vocab_size: 151936,
|
||||
hidden_size: 896,
|
||||
intermediate_size: 4864,
|
||||
num_hidden_layers: 24,
|
||||
num_attention_heads: 14,
|
||||
num_key_value_heads: 2,
|
||||
max_position_embeddings: 32768,
|
||||
sliding_window: 32768,
|
||||
max_window_layers: 21,
|
||||
tie_word_embeddings: true,
|
||||
rope_theta: 1000000.0,
|
||||
rms_norm_eps: 1e-6,
|
||||
use_sliding_window: false,
|
||||
hidden_act: candle_nn::Activation::Silu,
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let vb = unsafe {
|
||||
VarBuilder::from_mmaped_safetensors(&[model_path.clone()], dtype, &device)
|
||||
.map_err(|e| format!("VarBuilder: {}", e))?
|
||||
};
|
||||
let model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
||||
tracing::info!("Malli ladattu ({:.1}s) — {}", start.elapsed().as_secs_f64(), device_name);
|
||||
|
||||
Ok(LlmEngine {
|
||||
tokenizer,
|
||||
model,
|
||||
device,
|
||||
eos_token: 151645,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla → malli jatkaa suoraan koodilla
|
||||
let formatted = format!("<|im_start|>system\nYou are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n```\n", prompt);
|
||||
|
||||
let encoding = self.tokenizer.encode(formatted.as_str(), true)
|
||||
.map_err(|e| format!("Encode: {}", e))?;
|
||||
let input_ids: Vec<u32> = encoding.get_ids().to_vec();
|
||||
let input_len = input_ids.len();
|
||||
|
||||
// Nollataan KV-cache edellisestä promptista
|
||||
self.model.clear_kv_cache();
|
||||
|
||||
// Sampling-parametrit
|
||||
let temperature = 0.7;
|
||||
let top_k = 40;
|
||||
let repetition_penalty = 1.15;
|
||||
let mut rng_state: u64 = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Prefill
|
||||
let input = Tensor::new(input_ids.as_slice(), &self.device)
|
||||
.and_then(|t| t.unsqueeze(0))
|
||||
.map_err(|e| format!("Tensor: {}", e))?;
|
||||
|
||||
let logits = self.model.forward(&input, 0)
|
||||
.map_err(|e| format!("Forward prefill: {}", e))?;
|
||||
|
||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
|
||||
if seq_len == 0 { return Err("Tyhjä tensori".to_string()); }
|
||||
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
|
||||
// Jos OLLAMA_URL on asetettu, käytetään sitä suoraan
|
||||
let ollama_url = if let Ok(url) = std::env::var("OLLAMA_URL") {
|
||||
tracing::info!("Ollama backend (env): {}", url);
|
||||
url
|
||||
} else {
|
||||
logits
|
||||
};
|
||||
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
let mut all_tokens: Vec<u32> = Vec::new();
|
||||
|
||||
let mut next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
|
||||
|
||||
if next_token != self.eos_token {
|
||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
}
|
||||
all_tokens.push(next_token);
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
// Autoregressive
|
||||
let mut pos = input_len;
|
||||
for _ in 1..max_tokens {
|
||||
if next_token == self.eos_token { break; }
|
||||
|
||||
let input = Tensor::new(&[next_token], &self.device)
|
||||
.and_then(|t| t.unsqueeze(0))
|
||||
.map_err(|e| format!("Tensor: {}", e))?;
|
||||
|
||||
let logits = self.model.forward(&input, pos)
|
||||
.map_err(|e| format!("Forward pos {}: {}", pos, e))?;
|
||||
|
||||
let logits = logits.squeeze(0).map_err(|e| format!("Squeeze: {}", e))?;
|
||||
let logits = if logits.dims().len() == 2 {
|
||||
let seq_len = logits.dim(0).map_err(|e| format!("Dim: {}", e))?;
|
||||
if seq_len == 0 { break; }
|
||||
logits.get(seq_len - 1).map_err(|e| format!("Get: {}", e))?
|
||||
} else {
|
||||
logits
|
||||
};
|
||||
next_token = sample_top_k(&logits, top_k, temperature, &all_tokens, repetition_penalty, &mut rng_state)?;
|
||||
pos += 1;
|
||||
|
||||
if next_token == self.eos_token { break; }
|
||||
|
||||
if let Ok(text) = self.tokenizer.decode(&[next_token], true) {
|
||||
generated_text.push_str(&text);
|
||||
|
||||
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
||||
let lower = generated_text.to_lowercase();
|
||||
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
|
||||
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
|
||||
if let Some(pos) = generated_text.find(stop) {
|
||||
generated_text.truncate(pos);
|
||||
}
|
||||
}
|
||||
// Haistellaan Ollamaa tunnetuista osoitteista
|
||||
let candidates = [
|
||||
"http://localhost:11434",
|
||||
"http://127.0.0.1:11434",
|
||||
"http://ollama:11434",
|
||||
"http://host.docker.internal:11434",
|
||||
];
|
||||
let mut found = None;
|
||||
for url in &candidates {
|
||||
let probe = reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(2))
|
||||
.build().unwrap_or(client.clone());
|
||||
if let Ok(resp) = probe.get(format!("{}/api/version", url)).send().await {
|
||||
if resp.status().is_success() {
|
||||
tracing::info!("Ollama löytyi osoitteesta: {}", url);
|
||||
found = Some(url.to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
all_tokens.push(next_token);
|
||||
tokens_generated += 1;
|
||||
}
|
||||
found.unwrap_or_else(|| {
|
||||
tracing::warn!("Ollamaa ei löytynyt — käytetään oletusta http://localhost:11434");
|
||||
"http://localhost:11434".to_string()
|
||||
})
|
||||
};
|
||||
|
||||
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
|
||||
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
|
||||
}
|
||||
|
||||
let gen_time = start.elapsed();
|
||||
let tokens_per_sec = if gen_time.as_secs_f64() > 0.0 {
|
||||
tokens_generated as f64 / gen_time.as_secs_f64()
|
||||
pub fn model_name(&self) -> String {
|
||||
self.model.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn set_model(&self, new_model: String) {
|
||||
*self.model.borrow_mut() = new_model;
|
||||
}
|
||||
|
||||
/// Varmistaa että malli on ladattu Ollamaan (ollama pull)
|
||||
pub async fn ensure_model(&self) -> Result<(), String> {
|
||||
let model = self.model.borrow().clone();
|
||||
tracing::info!("Tarkistetaan malli {}...", model);
|
||||
let resp = self.client.post(format!("{}/api/pull", self.ollama_url))
|
||||
.json(&serde_json::json!({ "name": model, "stream": false }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama pull: {}", e))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
tracing::info!("Malli {} valmis", model);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Ollama pull epäonnistui: {}", resp.status()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
|
||||
let system = "You are a coding assistant. Respond with ONLY code. Use proper newlines and indentation. No explanations, no markdown fences, no comments unless asked.";
|
||||
let model = self.model.borrow().clone();
|
||||
|
||||
let start = Instant::now();
|
||||
let resp = self.client.post(format!("{}/api/generate", self.ollama_url))
|
||||
.json(&serde_json::json!({
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"system": system,
|
||||
"stream": false,
|
||||
"options": {
|
||||
"num_predict": max_tokens,
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"repeat_penalty": 1.15,
|
||||
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:"]
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Ollama generate: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Ollama HTTP {}", resp.status()));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.map_err(|e| format!("Ollama JSON: {}", e))?;
|
||||
|
||||
let text = body["response"].as_str().unwrap_or("").to_string();
|
||||
let total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
|
||||
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
|
||||
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as f64;
|
||||
let tokens_per_sec = if eval_duration_ns > 0 {
|
||||
eval_count as f64 / (eval_duration_ns as f64 / 1_000_000_000.0)
|
||||
} else { 0.0 };
|
||||
|
||||
Ok(GenerateResult {
|
||||
text: strip_markdown_wrapper(&generated_text),
|
||||
tokens_generated,
|
||||
duration_ms: gen_time.as_millis() as f64,
|
||||
text: strip_code_fences(&text),
|
||||
tokens_generated: eval_count,
|
||||
duration_ms,
|
||||
tokens_per_sec,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const LANG_TAGS: &[&str] = &[
|
||||
"python", "py", "rust", "rs", "javascript", "js", "typescript", "ts",
|
||||
"java", "kotlin", "scala", "go", "ruby", "rb", "php", "swift",
|
||||
"c", "cpp", "c++", "c#", "csharp", "r", "sql", "bash", "sh", "zsh",
|
||||
"html", "css", "json", "yaml", "yml", "toml", "xml", "markdown", "md",
|
||||
"lua", "perl", "dart", "elixir", "haskell", "hs", "ocaml", "zig",
|
||||
"plaintext", "text", "txt",
|
||||
];
|
||||
|
||||
/// Siivoa mallin tuottama vastaus (prefill-yhteensopiva).
|
||||
fn strip_markdown_wrapper(text: &str) -> String {
|
||||
/// Siivoa mahdolliset markdown-koodiblokki-merkit
|
||||
fn strip_code_fences(text: &str) -> String {
|
||||
let mut result = text.trim().to_string();
|
||||
|
||||
// 1. Kielitunniste — VAIN tunnettu kieli
|
||||
// Poista aloittava ```lang
|
||||
if result.starts_with("```") {
|
||||
if let Some(nl) = result.find('\n') {
|
||||
let first = result[..nl].trim().to_lowercase();
|
||||
if LANG_TAGS.contains(&first.as_str()) {
|
||||
result = result[nl + 1..].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Sulkeva ``` — VAIN omalla rivillään lopussa
|
||||
// Poista sulkeva ```
|
||||
let trimmed = result.trim_end();
|
||||
if trimmed.ends_with("```") {
|
||||
let before = &trimmed[..trimmed.len() - 3];
|
||||
@@ -264,29 +147,7 @@ fn strip_markdown_wrapper(text: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Johdantolauseet
|
||||
let lower = result.trim().to_lowercase();
|
||||
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
||||
if lower.starts_with(prefix) {
|
||||
if let Some(nl) = result.find('\n') { result = result[nl + 1..].to_string(); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Selityskommentit alusta
|
||||
let mut lines: Vec<&str> = result.trim().lines().collect();
|
||||
while !lines.is_empty() {
|
||||
let first = lines[0].trim();
|
||||
let is_preamble = first.starts_with("# ") && !first.starts_with("#!")
|
||||
&& (first.to_lowercase().contains("this is")
|
||||
|| first.to_lowercase().contains("simple")
|
||||
|| first.to_lowercase().contains("program that")
|
||||
|| first.to_lowercase().contains("here is")
|
||||
|| first.to_lowercase().contains("the following")
|
||||
|| first.to_lowercase().contains("below"));
|
||||
if is_preamble { lines.remove(0); } else { break; }
|
||||
}
|
||||
lines.join("\n").trim().to_string()
|
||||
result
|
||||
}
|
||||
|
||||
pub struct GenerateResult {
|
||||
|
||||
@@ -285,15 +285,19 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Ladataan LLM-malli
|
||||
tracing::info!("Ladataan LLM-mallia...");
|
||||
let mut llm = match inference::LlmEngine::load() {
|
||||
// Ollama-backend
|
||||
tracing::info!("Alustetaan Ollama-yhteyttä...");
|
||||
let llm = match inference::LlmEngine::load().await {
|
||||
Ok(engine) => {
|
||||
tracing::info!("LLM valmis inferenssiin!");
|
||||
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
|
||||
match engine.ensure_model().await {
|
||||
Ok(()) => tracing::info!("Ollama valmis inferenssiin!"),
|
||||
Err(e) => tracing::warn!("Mallin lataus: {} — yritetään silti", e),
|
||||
}
|
||||
Some(engine)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("LLM-lataus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
||||
tracing::warn!("Ollama-alustus epäonnistui: {} — toimitaan ilman inferenssiä", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -324,11 +328,13 @@ async fn main() {
|
||||
|
||||
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
|
||||
|
||||
if let Some(ref mut engine) = llm {
|
||||
if let Some(ref engine) = llm {
|
||||
busy = true;
|
||||
tracing::info!("Generoidaan (task_id: {}): \"{}\"", task_id, prompt);
|
||||
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
||||
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
|
||||
|
||||
match engine.generate(prompt, 64) {
|
||||
let model_name = engine.model_name();
|
||||
match engine.generate(prompt, max_tokens).await {
|
||||
Ok(result) => {
|
||||
tracing::info!(
|
||||
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
|
||||
@@ -341,7 +347,7 @@ async fn main() {
|
||||
let done = json!({
|
||||
"type": "llm_done",
|
||||
"prompt": prompt,
|
||||
"model": "Qwen2.5-Coder-0.5B (native/GPU)",
|
||||
"model": format!("{} (Ollama)", model_name),
|
||||
"response": result.text,
|
||||
"tokens_generated": result.tokens_generated,
|
||||
"duration_ms": result.duration_ms,
|
||||
@@ -360,7 +366,21 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ohitetaan pair_task, stats jne.
|
||||
// Mallin vaihto lennossa
|
||||
if text.contains("change_model") {
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if let Some(new_model) = task.get("model").and_then(|v| v.as_str()) {
|
||||
if let Some(ref engine) = llm {
|
||||
tracing::info!("Vaihdetaan malli: {}", new_model);
|
||||
engine.set_model(new_model.to_string());
|
||||
match engine.ensure_model().await {
|
||||
Ok(()) => tracing::info!("Malli {} valmis!", new_model),
|
||||
Err(e) => tracing::error!("Mallin lataus epäonnistui: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::warn!("Yhteys hubiin katkesi — yritetään uudelleen 5s...");
|
||||
|
||||
@@ -38,17 +38,50 @@ pub fn set_gpu_load(load: u32) {
|
||||
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
|
||||
}
|
||||
|
||||
// Asynkroninen odotus WebAssemblylle
|
||||
async fn sleep_ms(ms: i32) {
|
||||
// Worker-yhteensopiva setTimeout — toimii sekä Window- että Worker-kontekstissa
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = setTimeout)]
|
||||
fn set_timeout(closure: &js_sys::Function, ms: i32);
|
||||
}
|
||||
|
||||
// Asynkroninen odotus WebAssemblylle (Window + Worker)
|
||||
pub async fn sleep_ms(ms: i32) {
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
|
||||
.unwrap();
|
||||
set_timeout(&resolve, ms);
|
||||
});
|
||||
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||
}
|
||||
|
||||
// Worker-yhteensopiva Performance — käyttää globalThis.performance
|
||||
pub fn perf_now() -> f64 {
|
||||
js_sys::Reflect::get(&js_sys::global(), &"performance".into())
|
||||
.ok()
|
||||
.and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok())
|
||||
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
|
||||
.and_then(|f| {
|
||||
let perf = js_sys::Reflect::get(&js_sys::global(), &"performance".into()).unwrap();
|
||||
f.call0(&perf).ok()
|
||||
})
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
// Worker-yhteensopiva fetch — käyttää globalThis.fetch
|
||||
pub async fn worker_fetch(url: &str) -> Result<web_sys::Response, String> {
|
||||
let promise = js_sys::Reflect::get(&js_sys::global(), &"fetch".into())
|
||||
.map_err(|_| "fetch ei saatavilla".to_string())?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| "fetch ei funktio".to_string())?
|
||||
.call1(&JsValue::NULL, &url.into())
|
||||
.map_err(|e| format!("fetch: {:?}", e))?;
|
||||
let resp = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
|
||||
.await
|
||||
.map_err(|e| format!("fetch await: {:?}", e))?;
|
||||
resp.dyn_into::<web_sys::Response>()
|
||||
.map_err(|_| "ei Response".to_string())
|
||||
}
|
||||
|
||||
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
|
||||
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
|
||||
let device = Default::default();
|
||||
@@ -85,6 +118,27 @@ async fn run_ai_tensor_inference(difficulty: usize) -> String {
|
||||
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
|
||||
}
|
||||
|
||||
/// JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
|
||||
/// Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
|
||||
#[wasm_bindgen]
|
||||
pub async fn tokenize_js(text: String) -> Result<String, JsValue> {
|
||||
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
|
||||
let Some(bytes) = cached_tok else {
|
||||
// Yritetään ladata verkosta
|
||||
let resp = reqwest::get("https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B/resolve/main/tokenizer.json").await
|
||||
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
|
||||
let bytes = resp.bytes().await
|
||||
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
|
||||
let _ = storage::save_to_idb("tokenizer.json", &bytes).await;
|
||||
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
|
||||
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
|
||||
return Ok(tokenize_text(&tokenizer, &text).to_string());
|
||||
};
|
||||
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
|
||||
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
|
||||
Ok(tokenize_text(&tokenizer, &text).to_string())
|
||||
}
|
||||
|
||||
/// Tokenisoi yhden tekstin ja palauttaa metriikat
|
||||
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
|
||||
let char_count = text.chars().count();
|
||||
@@ -123,10 +177,9 @@ async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let Some(bytes) = cached_tok else { return; };
|
||||
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
||||
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let start = perf.now();
|
||||
let start = perf_now();
|
||||
let result = tokenize_text(&tokenizer, &text);
|
||||
let duration_ms = perf.now() - start;
|
||||
let duration_ms = perf_now() - start;
|
||||
|
||||
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
||||
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
@@ -157,11 +210,10 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
||||
return;
|
||||
};
|
||||
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let start_time = perf.now();
|
||||
let start_time = perf_now();
|
||||
let en_result = tokenize_text(&tokenizer, &en_text);
|
||||
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
||||
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
|
||||
let duration_ms = perf_now() - start_time;
|
||||
|
||||
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
|
||||
@@ -24,10 +24,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
|
||||
console_log!("[Qwen] Ladataan {}...", key);
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||
|
||||
let total_size: usize = resp.headers()
|
||||
@@ -71,7 +68,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
}
|
||||
|
||||
pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
// performance via crate::perf_now()
|
||||
|
||||
let tok_bytes = match ensure_cached("qwen05b-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||
Ok(b) => b,
|
||||
@@ -88,7 +85,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
};
|
||||
|
||||
console_log!("[Qwen] Rakennetaan mallia...");
|
||||
let start_load = perf.now();
|
||||
let start_load = crate::perf_now();
|
||||
let device = Device::Cpu;
|
||||
let dtype = DType::F32;
|
||||
|
||||
@@ -120,7 +117,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
Err(e) => { console_log!("[Qwen] Mallin lataus: {}", e); return; }
|
||||
};
|
||||
|
||||
let load_time = perf.now() - start_load;
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
console_log!("[Qwen] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
|
||||
let encoding = match tokenizer.encode(prompt.as_str(), true) {
|
||||
@@ -131,7 +128,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let input_len = input_ids.len();
|
||||
console_log!("[Qwen] Syöte: {} tokenia", input_len);
|
||||
|
||||
let start_gen = perf.now();
|
||||
let start_gen = crate::perf_now();
|
||||
let max_new_tokens = 32;
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
@@ -202,7 +199,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
crate::sleep_ms(0).await;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||
console_log!("[Qwen] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use candle_core::{Device, Tensor, DType};
|
||||
use candle_core::quantized::gguf_file;
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::models::qwen2::{Config as QwenConfig, ModelForCausalLM as QwenModel};
|
||||
use candle_transformers::models::quantized_qwen2::ModelWeights as QwenQuantizedModel;
|
||||
use wasm_bindgen::JsCast;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
@@ -16,13 +18,36 @@ macro_rules! console_log {
|
||||
const MODEL_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/model.safetensors";
|
||||
const TOKENIZER_05B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct/resolve/main/tokenizer.json";
|
||||
|
||||
// 3B — parempi laatu, vaatii enemmän muistia (~6 GB lataus, ~12 GB RAM)
|
||||
const MODEL_3B_PART1_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00001-of-00002.safetensors";
|
||||
const MODEL_3B_PART2_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/model-00002-of-00002.safetensors";
|
||||
const TOKENIZER_3B_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-3B-Instruct/resolve/main/tokenizer.json";
|
||||
// 1.5B GGUF Q4_K_M — kvantisoidtu, mahtuu selaimeen (~1 GB)
|
||||
const MODEL_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf";
|
||||
const TOKENIZER_GGUF_URL: &str = "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct/resolve/main/tokenizer.json";
|
||||
|
||||
enum CoderModel {
|
||||
Full(QwenModel),
|
||||
Quantized(QwenQuantizedModel),
|
||||
}
|
||||
|
||||
impl CoderModel {
|
||||
fn forward(&mut self, x: &Tensor, pos: usize) -> candle_core::Result<Tensor> {
|
||||
match self {
|
||||
CoderModel::Full(m) => m.forward(x, pos),
|
||||
CoderModel::Quantized(m) => m.forward(x, pos),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_kv_cache(&mut self) {
|
||||
match self {
|
||||
CoderModel::Full(m) => m.clear_kv_cache(),
|
||||
CoderModel::Quantized(_) => {
|
||||
// Quantized model nollaa KV-cachen automaattisesti kun forward kutsutaan pos=0:lla
|
||||
// (ks. quantized_qwen2.rs rivi 118: if index_pos == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedModel {
|
||||
model: QwenModel,
|
||||
model: CoderModel,
|
||||
tokenizer: tokenizers::Tokenizer,
|
||||
is_3b: bool,
|
||||
}
|
||||
@@ -115,10 +140,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
|
||||
console_log!("[Coder] Ladataan {}...", key);
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
||||
.await.map_err(|e| format!("Fetch: {:?}", e))?;
|
||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response".to_string())?;
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
if !resp.ok() { return Err(format!("HTTP {}", resp.status())); }
|
||||
|
||||
let total_size: usize = resp.headers()
|
||||
@@ -182,50 +204,39 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
|
||||
let dtype = DType::F32;
|
||||
|
||||
// Tokenizer
|
||||
let tok_url = if use_3b { TOKENIZER_3B_URL } else { TOKENIZER_05B_URL };
|
||||
let tok_key = if use_3b { "coder3b-tokenizer.json" } else { "coder05b-tokenizer.json" };
|
||||
let tok_url = if use_3b { TOKENIZER_GGUF_URL } else { TOKENIZER_05B_URL };
|
||||
let tok_key = if use_3b { "coder15b-tokenizer.json" } else { "coder05b-tokenizer.json" };
|
||||
let tok_bytes = ensure_cached(tok_key, tok_url, ws).await?;
|
||||
let tokenizer = tokenizers::Tokenizer::from_bytes(&tok_bytes[..])
|
||||
.map_err(|e| format!("Tokenizer: {}", e))?;
|
||||
|
||||
// Painot
|
||||
let tensors = if use_3b {
|
||||
let part1 = ensure_cached("coder3b-model-part1.safetensors", MODEL_3B_PART1_URL, ws).await?;
|
||||
let part2 = ensure_cached("coder3b-model-part2.safetensors", MODEL_3B_PART2_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan 3B-mallia...");
|
||||
let mut all_tensors = candle_core::safetensors::load_buffer(&part1[..], &device)
|
||||
.map_err(|e| format!("Part1: {}", e))?;
|
||||
let tensors2 = candle_core::safetensors::load_buffer(&part2[..], &device)
|
||||
.map_err(|e| format!("Part2: {}", e))?;
|
||||
all_tensors.extend(tensors2);
|
||||
all_tensors
|
||||
let model = if use_3b {
|
||||
// GGUF Q4_K_M — kvantisoidtu 3B-malli (~1.9 GB)
|
||||
let gguf_bytes = ensure_cached("coder15b-q4km.gguf", MODEL_GGUF_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan kvantisoidun 1.5B-mallia (Q4_K_M)...");
|
||||
let mut cursor = std::io::Cursor::new(&gguf_bytes[..]);
|
||||
let content = gguf_file::Content::read(&mut cursor)
|
||||
.map_err(|e| format!("GGUF parse: {}", e))?;
|
||||
let qmodel = QwenQuantizedModel::from_gguf(content, &mut cursor, &device)
|
||||
.map_err(|e| format!("GGUF model: {}", e))?;
|
||||
CoderModel::Quantized(qmodel)
|
||||
} else {
|
||||
let model_bytes = ensure_cached("coder05b-model.safetensors", MODEL_05B_URL, ws).await?;
|
||||
console_log!("[Coder] Rakennetaan 0.5B-mallia...");
|
||||
candle_core::safetensors::load_buffer(&model_bytes[..], &device)
|
||||
.map_err(|e| format!("Safetensors: {}", e))?
|
||||
};
|
||||
|
||||
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
|
||||
let config = if use_3b {
|
||||
QwenConfig {
|
||||
vocab_size: 151936, hidden_size: 2048, intermediate_size: 11008,
|
||||
num_hidden_layers: 36, num_attention_heads: 16, num_key_value_heads: 2,
|
||||
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 36,
|
||||
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
|
||||
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu,
|
||||
}
|
||||
} else {
|
||||
QwenConfig {
|
||||
let tensors = candle_core::safetensors::load_buffer(&model_bytes[..], &device)
|
||||
.map_err(|e| format!("Safetensors: {}", e))?;
|
||||
let config = QwenConfig {
|
||||
vocab_size: 151936, hidden_size: 896, intermediate_size: 4864,
|
||||
num_hidden_layers: 24, num_attention_heads: 14, num_key_value_heads: 2,
|
||||
max_position_embeddings: 32768, sliding_window: 32768, max_window_layers: 21,
|
||||
tie_word_embeddings: true, rope_theta: 1000000.0, rms_norm_eps: 1e-6,
|
||||
use_sliding_window: false, hidden_act: candle_nn::Activation::Silu,
|
||||
}
|
||||
};
|
||||
|
||||
let model = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
||||
let vb = VarBuilder::from_tensors(tensors, dtype, &device);
|
||||
let qwen = QwenModel::new(&config, vb).map_err(|e| format!("Malli: {}", e))?;
|
||||
CoderModel::Full(qwen)
|
||||
};
|
||||
console_log!("[Coder] Malli ladattu ja välimuistitettu");
|
||||
|
||||
MODEL_CACHE.with(|c| {
|
||||
@@ -237,17 +248,16 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
|
||||
|
||||
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
|
||||
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let size_label = if use_3b { "3B" } else { "0.5B" };
|
||||
|
||||
let start_load = perf.now();
|
||||
let start_load = crate::perf_now();
|
||||
|
||||
if let Err(e) = get_or_build_model(use_3b, &ws).await {
|
||||
console_log!("[Coder] Mallin lataus: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let load_time = perf.now() - start_load;
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
if load_time > 100.0 {
|
||||
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
}
|
||||
@@ -258,13 +268,13 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
||||
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
||||
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
|
||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(256) as usize;
|
||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
|
||||
(p, s, m)
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 256)
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
}
|
||||
} else {
|
||||
(prompt.clone(), default_system.to_string(), 256)
|
||||
(prompt.clone(), default_system.to_string(), 512)
|
||||
};
|
||||
|
||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
|
||||
@@ -283,7 +293,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
console_log!("[Coder] Syöte: {} tokenia", input_len);
|
||||
|
||||
let device = Device::Cpu;
|
||||
let start_gen = perf.now();
|
||||
let start_gen = crate::perf_now();
|
||||
let eos_token = 151645u32;
|
||||
let temperature: f32 = 0.7;
|
||||
let top_k: usize = 40;
|
||||
@@ -359,7 +369,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
|
||||
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
|
||||
let cleaned = strip_markdown_wrapper(&generated_text);
|
||||
|
||||
@@ -28,10 +28,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
||||
send_progress(ws, key, 0, 0, 0);
|
||||
|
||||
// Fetch API:lla saadaan Content-Length ja streaming-luku
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url))
|
||||
.await.map_err(|e| format!("Fetch epäonnistui: {:?}", e))?;
|
||||
let resp: web_sys::Response = resp_val.dyn_into().map_err(|_| "Ei Response-objekti".to_string())?;
|
||||
let resp = crate::worker_fetch(url).await?;
|
||||
|
||||
if !resp.ok() {
|
||||
return Err(format!("HTTP {}", resp.status()));
|
||||
@@ -99,7 +96,7 @@ fn send_progress(ws: &Rc<RefCell<WebSocket>>, file: &str, pct: u32, loaded: usiz
|
||||
|
||||
/// Lataa malli ja tokenizer, suorita inferenssi ja streamaa tokenit hubille
|
||||
pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
// performance via crate::perf_now()
|
||||
|
||||
// 1. Lataa tokenizer
|
||||
let tok_bytes = match ensure_cached("smollm-tokenizer.json", TOKENIZER_URL, &ws).await {
|
||||
@@ -122,7 +119,7 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
|
||||
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
|
||||
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
|
||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws).await;
|
||||
}
|
||||
|
||||
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
@@ -130,9 +127,8 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
model_bytes: Vec<u8>,
|
||||
tokenizer: tokenizers::Tokenizer,
|
||||
ws: Rc<RefCell<WebSocket>>,
|
||||
perf: web_sys::Performance, // Korjattu Wasm-performanssi välitettäväksi
|
||||
) {
|
||||
let start_load = perf.now();
|
||||
let start_load = crate::perf_now();
|
||||
|
||||
let device = Default::default();
|
||||
let config = crate::burn_smollm::config::SmolLMConfig::default();
|
||||
@@ -143,7 +139,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
Err(e) => { console_log!("[SmolLM] Lataus epäonnistui: {}", e); return; }
|
||||
};
|
||||
|
||||
let load_time = perf.now() - start_load;
|
||||
let load_time = crate::perf_now() - start_load;
|
||||
console_log!("[SmolLM] Burn-malli ladattu ({:.0}ms). Generoidaan...", load_time);
|
||||
|
||||
let formatted_prompt = format!("<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
|
||||
@@ -156,7 +152,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
let input_len = input_ids.len();
|
||||
console_log!("[SmolLM] Syöte: {} tokenia", input_len);
|
||||
|
||||
let start_gen = perf.now();
|
||||
let start_gen = crate::perf_now();
|
||||
let max_new_tokens = 32;
|
||||
let mut generated_text = String::new();
|
||||
let mut tokens_generated: usize = 0;
|
||||
@@ -219,7 +215,7 @@ async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
tokens_generated += 1;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
let gen_time = crate::perf_now() - start_gen;
|
||||
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };
|
||||
|
||||
let done = serde_json::json!({
|
||||
|
||||
413
network-poc/static/GUIDE.md
Normal file
413
network-poc/static/GUIDE.md
Normal file
@@ -0,0 +1,413 @@
|
||||
# Kipinä Agentic Studio — Opas
|
||||
|
||||
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
|
||||
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
|
||||
tuloksia voi parantaa.
|
||||
|
||||
---
|
||||
|
||||
## Kielimallit ja niiden koot
|
||||
|
||||
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
|
||||
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
|
||||
|
||||
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|
||||
|-------|-----------|-------------|-------------------|-------------|
|
||||
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
|
||||
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
|
||||
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
|
||||
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
|
||||
|
||||
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
|
||||
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
|
||||
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
|
||||
ja oppikirjan välillä.
|
||||
|
||||
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
|
||||
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
|
||||
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
|
||||
|
||||
---
|
||||
|
||||
## Tokenit — kielimallin "sanat"
|
||||
|
||||
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
|
||||
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
|
||||
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
|
||||
merkkijonot harjoitusdatasta.
|
||||
|
||||
### Esimerkki: suomi vs. englanti
|
||||
|
||||
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
|
||||
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
|
||||
tokeneita saman merkityksen välittämiseen:
|
||||
|
||||

|
||||
|
||||
**Huomaa miten:**
|
||||
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
|
||||
- Suomen sanat pilkotaan pienempiin osiin (`Hajautettu` → 4 tokenia, `Distributed` → 2)
|
||||
- Suomi vaatii **30-50% enemmän tokeneita** saman merkityksen välittämiseen
|
||||
- Koodiavainsanat (`function`, `list`, `sort`) ovat tehokkaita molemmilla kielillä
|
||||
|
||||
### Miksi tämä merkitsee?
|
||||
|
||||
**Jokainen tokeni = yksi laskentakierros.** Jos suomi vaatii 50% enemmän tokeneita:
|
||||
|
||||
1. **Hitaampi vastaus:** 100 tokenin englanninkielinen vastaus ≈ 150 tokenia suomeksi
|
||||
→ 50% pidempi odotusaika
|
||||
2. **Pienempi konteksti:** Sama merkityssisältö vie enemmän tilaa konteksti-ikkunasta
|
||||
3. **Huonompi ymmärrys:** Pitkät sanat pilkotaan osiin jotka malli ei välttämättä
|
||||
tunnista → hallusinaatiot lisääntyvät
|
||||
|
||||
**Siksi tekniset promptit ovat englanniksi** — malli saa enemmän informaatiota
|
||||
samassa token-budjetissa ja ymmärtää ohjeet paremmin.
|
||||
|
||||
**Token-budjetti tässä järjestelmässä:**
|
||||
|
||||
| Osa | Tokeneita | Osuus |
|
||||
|-----|-----------|-------|
|
||||
| System prompt | ~30 | kiinteä |
|
||||
| Agent prompt | ~25 | kiinteä |
|
||||
| Konteksti (aiemmat tiedostot) | 0-300 | kasvaa |
|
||||
| Käyttäjän prompti | ~20-50 | vaihtelee |
|
||||
| **Syöte yhteensä** | **~75-400** | |
|
||||
| Generoitu vastaus (max) | 512 | raja |
|
||||
| **Yhteensä** | **~600-900** | /32 768 |
|
||||
|
||||
Konteksti-ikkuna on reilusti riittävä. Pullonkaula ei ole ikkunan koko
|
||||
vaan **mallin kyky ymmärtää pitkää kontekstia** — 0.5B-malli alkaa
|
||||
"unohtaa" ohjeet kun konteksti kasvaa yli ~200 tokenin.
|
||||
|
||||
---
|
||||
|
||||
## Promptit — miten mallia ohjataan
|
||||
|
||||
### Kolmitasoinen prompttirakenne
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["System prompt<br/><i>You are a coding assistant. Respond with ONLY code.</i><br/>🔒 Kiinteä, kovakoodattu — malli priorisoi tämän"]
|
||||
A["Agent prompt<br/><i>Olet kokenut ohjelmistokehittäjä...</i><br/>✏️ Käyttäjän muokattavissa UI:ssa"]
|
||||
U["User prompt<br/><i>Write ONLY the file main.py...</i><br/>📋 Vaihtelee joka kutsussa, sisältää kontekstin"]
|
||||
P["Prefill: ``` <br/>🎯 Pakottaa mallin aloittamaan koodilla"]
|
||||
S --> A --> U --> P
|
||||
P -->|malli jatkaa| R["Generoitu koodi"]
|
||||
|
||||
style S fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
style A fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
|
||||
style U fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
|
||||
style P fill:#1a1e2e,stroke:#a371f7,color:#c9d1d9
|
||||
style R fill:#0d1117,stroke:#58a6ff,color:#58a6ff
|
||||
```
|
||||
|
||||
### Miksi promptit ovat englanniksi?
|
||||
|
||||
Qwen2.5-Coder on harjoitettu pääosin englanninkielisellä koodilla ja
|
||||
dokumentaatiolla. Suomenkielinen ohje kuluttaa enemmän tokeneita JA
|
||||
malli ymmärtää sen huonommin. Agenttien nimet ja käyttöliittymä ovat
|
||||
suomeksi, mutta tekniset ohjeet mallille englanniksi.
|
||||
|
||||
Poikkeus: agenttipromptit ovat suomeksi koska ne menevät user-blokkiin
|
||||
(ei system-blokkiin) ja niiden tarkoitus on enemmän "persoonallisuus"
|
||||
kuin tekninen ohje.
|
||||
|
||||
---
|
||||
|
||||
## Prefill-tekniikka
|
||||
|
||||
Normaalisti malli päättää vapaasti miten vastaa:
|
||||
|
||||
```
|
||||
Ilman prefilliä:
|
||||
Malli: "Sure! Here is a Python program that prints Hello World:\n```python\nprint('Hello')\n```"
|
||||
→ 25 tokenia, joista 15 turhia
|
||||
|
||||
Prefillin kanssa:
|
||||
Me syötämme: ```
|
||||
Malli jatkaa: python\nprint('Hello')\n```
|
||||
→ 5 tokenia, kaikki hyödyllisiä
|
||||
```
|
||||
|
||||
Prefill on kuin aloittaisit lauseen toisen puolesta — malli jatkaa
|
||||
siitä mihin jäit sen sijaan, että aloittaisi kohteliaalla johdannolla.
|
||||
|
||||
**Sivuvaikutus:** Malli tuottaa kielitunnisteen (`python`, `rust`) ja
|
||||
sulkevan ` ``` `:n. Nämä siivotaan jälkikäteen `strip_markdown_wrapper`-funktiolla.
|
||||
|
||||
---
|
||||
|
||||
## Sampling — miten malli valitsee seuraavan tokenin
|
||||
|
||||
Malli ei "tiedä" oikeaa vastausta. Se laskee jokaiselle mahdolliselle
|
||||
seuraavalle tokenille todennäköisyyden ja valitsee yhden. Valintaa
|
||||
ohjataan kolmella parametrilla:
|
||||
|
||||
### Temperature (0.7)
|
||||
|
||||
Kontrolloi "luovuutta" vs. "varmuutta":
|
||||
|
||||
```
|
||||
Temperature 0.0 (greedy): Aina todennäköisin tokeni → "def fibonacci(n):"
|
||||
Temperature 0.7 (oletus): Painottaa todennäköisiä mutta sallii vaihtelua
|
||||
Temperature 1.5 (luova): Lähes satunnainen → "async lambda fib = ..."
|
||||
```
|
||||
|
||||
0.7 on kompromissi: tarpeeksi determinististä tuottamaan toimivaa koodia,
|
||||
mutta tarpeeksi vaihtelevaa välttämään toistoa.
|
||||
|
||||
### Top-k (40)
|
||||
|
||||
Rajaa valinnan 40 todennäköisimpään tokeniin. Estää mallia valitsemasta
|
||||
täysin absurdeja vaihtoehtoja:
|
||||
|
||||
```
|
||||
Ilman top-k: 150 936 vaihtoehtoa → voi valita minkä tahansa
|
||||
Top-k 40: 40 vaihtoehtoa → järkevät vaihtoehdot
|
||||
Top-k 1: 1 vaihtoehto → greedy (aina sama vastaus)
|
||||
```
|
||||
|
||||
### Repetition penalty (1.15)
|
||||
|
||||
Vähentää jo tuotettujen tokenien todennäköisyyttä. Estää mallia
|
||||
juuttumasta luuppiin:
|
||||
|
||||
```
|
||||
Ilman rangaistusta: "print print print print print..."
|
||||
Penalty 1.15: "print('Hello')\nprint('World')"
|
||||
```
|
||||
|
||||
1.15 on lievä rangaistus — estää pahimman toiston mutta sallii
|
||||
saman avainsanan (esim. `return`) esiintymisen useasti.
|
||||
|
||||
---
|
||||
|
||||
## Stop-sekvenssit — milloin generointi loppuu
|
||||
|
||||
Malli generoi tokeneita kunnes jokin näistä tapahtuu:
|
||||
|
||||
1. **EOS-tokeni** (151645): Mallin oma "loppu"-merkki
|
||||
2. **Max tokens** (512): Kovakoodattu raja
|
||||
3. **Stop-sekvenssi**: Malli alkaa tuottaa selitystä
|
||||
|
||||
```
|
||||
fn fibonacci(n: usize) -> usize {
|
||||
if n <= 1 { return n; }
|
||||
fibonacci(n-1) + fibonacci(n-2)
|
||||
}
|
||||
← Tähän asti koodia, ok
|
||||
// Example usage: ← Stop! Tämä ei ole enää vastausta
|
||||
let result = fibonacci(10); ← Ei generoida
|
||||
```
|
||||
|
||||
Tunnistetut stop-sekvenssit: `### `, `Explanation`, `Note:`, `Output:`,
|
||||
`// Example`, `# Example`. Generointi katkaistaan ja teksti trimmataan
|
||||
stop-kohtaan.
|
||||
|
||||
---
|
||||
|
||||
## Projekti-pipeline — miten agenttitiimi toimii
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U["Käyttäjä: FastAPI + SQLite REST API for users"] --> M
|
||||
M["🟡 Manageri: Pilko tiedostoiksi"] -->|tiedostolista| C1
|
||||
C1["🟢 Koodari: models.py"] -->|"konteksti: models.py"| C2
|
||||
C2["🟢 Koodari: main.py"] -->|"konteksti: models + main"| C3
|
||||
C3["🟢 Koodari: pyproject.toml"] -->|kaikki tiedostot| T1
|
||||
T1["🔵 Testaaja: Review"] -->|bugeja löytyi| C4
|
||||
T1 -->|LGTM| Done["✅ Projekti valmis"]
|
||||
C4["🟡 Koodari: Korjaukset"] --> T2
|
||||
T2["🔵 Testaaja: Uudelleenarviointi"] --> Done
|
||||
```
|
||||
|
||||
**Kontekstin ketjutus** on kriittistä: kun koodari kirjoittaa `main.py`:tä,
|
||||
se saa `models.py`:n sisällön promptissa. Ilman tätä se ei tietäisi
|
||||
mitä luokkia importata.
|
||||
|
||||
**Riippuvuusjärjestys:** Manageria pyydetään listaamaan riippuvuudet ensin
|
||||
(models.py ennen main.py) jotta kontekstiketju toimii oikeaan suuntaan.
|
||||
|
||||
---
|
||||
|
||||
## Laadun parantaminen
|
||||
|
||||
### 1. Isompi malli (suurin vaikutus)
|
||||
|
||||
| | 0.5B | 3B | Pilvi-API |
|
||||
|---|---|---|---|
|
||||
| Fibonacci | Joskus virheitä | Yleensä oikein | Aina oikein |
|
||||
| FastAPI CRUD | Voi käyttää Flaskia | Oikea kirjasto | Täydellinen |
|
||||
| Monimutkainen logiikka | Hallusinoi | Osaa perusasiat | Syvä ymmärrys |
|
||||
| Nopeus (selain) | ~5 tok/s | ~0.4 tok/s | — |
|
||||
| Latauksen koko | 990 MB | 6.2 GB | 0 (API) |
|
||||
|
||||
**Käytännössä:** `kpn load 2` lataa 3B-mallin. Hitaampi mutta huomattavasti
|
||||
parempi koodinlaatu. Suositus monimutkaisiin projekteihin.
|
||||
|
||||
### 2. Paremmat promptit (ilmaista)
|
||||
|
||||
**Huono:** `"tee fibonacci"`
|
||||
- Malli ei tiedä kieltä, formaattia tai kontekstia
|
||||
|
||||
**Hyvä:** `"Write a fibonacci function in Rust that returns Vec<u64>"`
|
||||
- Kieli, palautustyyppi ja rakenne määritelty
|
||||
|
||||
**Promptin säännöt:**
|
||||
- Englanniksi (tehokkaampi tokenisointi, parempi ymmärrys)
|
||||
- Konkreettinen (mainitse kieli, kirjastot, palautustyyppi)
|
||||
- Lyhyt (jokainen sana kuluttaa tokenin konteksti-ikkunasta)
|
||||
- Positiivinen ("Write X" ei "Don't write Y")
|
||||
|
||||
### 3. Kontekstin hallinta (pipeline-taso)
|
||||
|
||||
**Ongelma:** 0.5B-malli "unohtaa" promptin alun kun konteksti kasvaa.
|
||||
|
||||
**Ratkaisu:** Pienet, kohdennetut promptit:
|
||||
- Yksi tiedosto kerrallaan (ei "kirjoita koko projekti")
|
||||
- Vain relevantit aiemmat tiedostot kontekstina
|
||||
- Max 4 tiedostoa per projekti
|
||||
|
||||
### 4. Iterointi (review-luuppi)
|
||||
|
||||
Yksi generointikierros tuottaa harvoin virheetöntä koodia.
|
||||
Pipeline-arkkitehtuuri mahdollistaa:
|
||||
|
||||
1. **Generointi** — ensimmäinen versio
|
||||
2. **Review** — testaaja löytää ongelmat
|
||||
3. **Korjaus** — koodari saa palautteen ja korjaa
|
||||
4. **Uusi review** — tarkistetaan korjaukset
|
||||
|
||||
Nykyinen järjestelmä tekee max 1 korjauskierroksen. Useampi
|
||||
iteraatio parantaisi laatua mutta kasvattaisi laskenta-aikaa.
|
||||
|
||||
### 5. Erikoistetut system promptit
|
||||
|
||||
Oletuspromptit ovat yleiskäyttöisiä. Projektikohtaiset promptit
|
||||
parantavat laatua merkittävästi:
|
||||
|
||||
```
|
||||
Oletus: "Olet kokenut ohjelmistokehittäjä."
|
||||
|
||||
Parempi: "You are a Python backend developer specializing in FastAPI.
|
||||
Always use Pydantic models for request/response schemas.
|
||||
Always use dependency injection for database sessions.
|
||||
Follow the repository pattern."
|
||||
```
|
||||
|
||||
Agenttikohtaiset promptit voi muokata suoraan UI:ssa.
|
||||
|
||||
### 6. Few-shot esimerkit
|
||||
|
||||
Malli oppii parhaiten esimerkeistä. Sen sijaan, että sanot "kirjoita
|
||||
FastAPI endpoint", näytä miltä haluat tuloksen näyttävän:
|
||||
|
||||
```
|
||||
Write a GET endpoint like this example:
|
||||
|
||||
@app.get("/items")
|
||||
def list_items():
|
||||
db = SessionLocal()
|
||||
return db.query(Item).all()
|
||||
|
||||
Now write a similar endpoint for /users.
|
||||
```
|
||||
|
||||
0.5B-malli jäljittelee rakennetta tehokkaasti — se on parempi kopioimaan
|
||||
kuin keksimään. Nykyinen pyproject.toml-esimerkki promptissa on tätä tekniikkaa.
|
||||
|
||||
### 7. Temperature-säätö tehtävän mukaan
|
||||
|
||||
Nykyinen temperature 0.7 on kompromissi. Eri tehtävät hyötyisivät eri arvoista:
|
||||
|
||||
| Tehtävä | Paras temperature | Miksi |
|
||||
|---------|-------------------|-------|
|
||||
| Tarkka koodi (CRUD, boilerplate) | 0.2-0.4 | Determinismi tärkeää |
|
||||
| Luova koodi (algoritmit, arkkitehtuuri) | 0.6-0.8 | Vaihtelu löytää ratkaisuja |
|
||||
| Vapaa teksti (kommentit, dokumentaatio) | 0.8-1.0 | Luonnollisempi kieli |
|
||||
|
||||
Järjestelmä voisi valita temperaturen automaattisesti tehtävätyypin perusteella.
|
||||
|
||||
### 8. Ensemble — sama prompti usealle mallille
|
||||
|
||||
Lähetetään sama tehtävä kahdelle solmulle ja valitaan parempi vastaus.
|
||||
Nykyinen Proof of Compute -arkkitehtuuri tukee tätä periaatteessa:
|
||||
hub voisi reitittää saman task_id:n kahdelle solmulle ja verrata tuloksia.
|
||||
|
||||
Käytännössä tämä kaksinkertaistaa laskenta-ajan mutta parantaa laatua
|
||||
merkittävästi — virheellinen vastaus harvoin on sama kahdella ajolla
|
||||
koska sampling on stokastinen.
|
||||
|
||||
### 9. Post-processing (nykyinen)
|
||||
|
||||
Mallin raakavastaus siivotaan:
|
||||
1. Kielitunniste poistetaan (`python`, `rust`, ...)
|
||||
2. Sulkeva ` ``` ` poistetaan
|
||||
3. Johdantolauseet poistetaan ("Sure!", "Here is...")
|
||||
4. Selityskommentit poistetaan ("# This is a simple...")
|
||||
5. Stop-sekvenssit katkaisevat generoinnin
|
||||
|
||||
Tämä ei paranna mallin ajattelua mutta poistaa turhan roskan.
|
||||
|
||||
### 10. Mallin hienosäätö (fine-tuning)
|
||||
|
||||
Qwen2.5-Coder on yleiskäyttöinen koodimalli. Jos sitä hienosäätäisi
|
||||
omalla koodiaineistolla (esim. yrityksen koodikanta, tietty framework),
|
||||
se tuottaisi huomattavasti parempaa koodia juuri siihen kontekstiin.
|
||||
|
||||
LoRA-hienosäätö 0.5B-mallille vaatii ~4 GB GPU-muistia ja muutaman
|
||||
tunnin harjoittelua. Tulos on erikoistunut malli joka osaa tuottaa
|
||||
esimerkiksi juuri FastAPI + SQLAlchemy -koodia luotettavasti.
|
||||
|
||||
---
|
||||
|
||||
## Välimuistiarkkitehtuuri — miksi toinen lataus on nopea
|
||||
|
||||
```
|
||||
Ensimmäinen lataus (hidas):
|
||||
Verkko (HuggingFace CDN) → IndexedDB → RAM → Mallin rakennus
|
||||
~990 MB lataus, ~30-60s
|
||||
|
||||
Toinen lataus samalla sivulatauksella (nopea):
|
||||
RAM-cache → Mallia ei rakenneta uusiksi, vain KV-cache nollataan
|
||||
~0ms
|
||||
|
||||
Refresh jälkeen (keskitaso):
|
||||
IndexedDB → RAM → Mallin rakennus
|
||||
~0 MB lataus, ~2-5s rakennus
|
||||
|
||||
Uusi selain/laite (hidas):
|
||||
Verkko → IndexedDB → RAM → Mallin rakennus
|
||||
Kuten ensimmäinen lataus
|
||||
```
|
||||
|
||||
**KV-cache:** Mallin sisäinen muisti joka tallentaa aiempien tokenien
|
||||
laskenta tulokset. Nollataan (`clear_kv_cache()`) jokaisen promptin
|
||||
välillä jotta edellinen vastaus ei vuoda seuraavaan.
|
||||
|
||||
---
|
||||
|
||||
## Lukuja käytännöstä
|
||||
|
||||
**Yksittäinen funktio** (esim. fibonacci):
|
||||
- Input: ~80 tokenia
|
||||
- Output: ~50-100 tokenia
|
||||
- Aika: ~10-20s (0.5B, selain)
|
||||
- Laatu: Yleensä toimiva, joskus loogisia virheitä
|
||||
|
||||
**3 tiedoston projekti** (esim. FastAPI CRUD):
|
||||
- Manageri: ~30 tok out
|
||||
- Koodari (3x): ~100-150 tok out per tiedosto
|
||||
- Testeri: ~50 tok out
|
||||
- Korjaukset: ~100 tok out (jos tarpeen)
|
||||
- **Yhteensä: ~500-700 tokenia, ~3-5 min**
|
||||
- Laatu: Rakenne oikein, yksittäisiä bugeja
|
||||
|
||||
**Token-kustannus vs. pilvipalvelu:**
|
||||
- Tässä järjestelmässä: 0 euroa (laskenta omalla koneella)
|
||||
- GPT-4 API: ~700 tokenia x $0.03/1K = ~$0.02 per projekti
|
||||
- Claude API: ~700 tokenia x $0.015/1K = ~$0.01 per projekti
|
||||
|
||||
Selaimessa ajettava malli on ilmainen mutta huomattavasti hitaampi
|
||||
ja heikompilaatuinen kuin pilvi-API. Sopii oppimiseen, prototypointiin
|
||||
ja tilanteisiin joissa data ei saa lähteä omalta koneelta.
|
||||
BIN
network-poc/static/images/tokenization-example.png
Normal file
BIN
network-poc/static/images/tokenization-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
File diff suppressed because it is too large
Load Diff
38
network-poc/static/worker.js
Normal file
38
network-poc/static/worker.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// Kipinä WASM Worker (ES module) — ajaa kielimallin inferenssin erillisessä säikeessä
|
||||
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
|
||||
|
||||
let wasmReady = false;
|
||||
|
||||
// Välitetään console.log -viestit pääsäikeelle jotta UI-kuuntelijat näkevät ne
|
||||
const _origLog = console.log;
|
||||
console.log = function(...args) {
|
||||
_origLog.apply(console, args);
|
||||
self.postMessage({ type: 'log', message: args.join(' ') });
|
||||
};
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
const { type, data } = e.data;
|
||||
|
||||
if (type === 'init') {
|
||||
try {
|
||||
await init();
|
||||
wasmReady = true;
|
||||
self.postMessage({ type: 'ready' });
|
||||
} catch (err) {
|
||||
self.postMessage({ type: 'error', message: 'WASM init: ' + err.message });
|
||||
}
|
||||
} else if (type === 'start') {
|
||||
if (!wasmReady) return;
|
||||
const { hubUrl, hasWebGPU, deviceInfo, taskId } = data;
|
||||
try {
|
||||
await start_agent_node(hubUrl, hasWebGPU, deviceInfo, taskId);
|
||||
self.postMessage({ type: 'started' });
|
||||
} catch (err) {
|
||||
self.postMessage({ type: 'error', message: 'Node: ' + err.message });
|
||||
}
|
||||
} else if (type === 'set_gpu_load') {
|
||||
if (wasmReady) set_gpu_load(data.load);
|
||||
} else if (type === 'set_auto_tasks') {
|
||||
if (wasmReady) set_auto_tasks(data.enabled);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user