mod common;
use common::ranks;
use nuvec::distributed::Group;
use nuvec::{Array, DType, Quant, random};
use nuvec_nn::sharded::{AllToShardedLinear, ShardedToAllLinear};
use nuvec_nn::{Linear, QuantizedLinear};
fn close(name: &str, got: &Array, want: &Array, tol: f32) {
let (g, w) = (got.to_vec::<f32>().unwrap(), want.to_vec::<f32>().unwrap());
assert_eq!(got.shape(), want.shape(), "{name}");
let worst = g
.iter()
.zip(&w)
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
assert!(worst <= tol, "{name}: off by {worst}");
}
fn layers(g: Group) {
random::seed(3);
let x = random::normal([3, 64], DType::F32, 0.0, 1.0, None);
let up = Linear::new(64, 128);
let down = Linear::new(128, 64);
let up_r = AllToShardedLinear::from_linear(&up, &g).unwrap();
let down_r = ShardedToAllLinear::from_linear(&down, &g).unwrap();
let want = down.forward(&up.forward(&x).sigmoid());
let got = down_r.forward(&up_r.forward(&x).sigmoid());
close("linear pair", &got, &want, 1e-4);
let (r, n) = (g.rank() as i32, g.size() as i32);
let per = 128 / n;
close(
"all-to-sharded slice",
&up_r.forward(&x),
&up.forward(&x).i((.., r * per..(r + 1) * per)),
1e-5,
);
let (qup, qdown) = (
QuantizedLinear::from_linear_with(&up, Quant::affine(32, 4)),
QuantizedLinear::from_linear_with(&down, Quant::affine(32, 4)),
);
let want = qdown.forward(&qup.forward(&x).sigmoid());
let got = ShardedToAllLinear::from_quantized(&qdown, &g)
.unwrap()
.forward(
&AllToShardedLinear::from_quantized(&qup, &g)
.unwrap()
.forward(&x)
.sigmoid(),
);
close("quantized pair", &got, &want, 1e-4);
let odd = Linear::new(64, 6 * n + 1);
let err = AllToShardedLinear::from_linear(&odd, &g)
.unwrap_err()
.to_string();
assert!(
err.contains("AllToShardedLinear") && err.contains("output features"),
"{err}"
);
let err = ShardedToAllLinear::from_quantized(
&QuantizedLinear::from_linear_with(&Linear::new(32 * n + 32, 8), Quant::affine(32, 4)),
&g,
);
if n > 1 {
assert!(err.unwrap_err().to_string().contains("ShardedToAllLinear"));
}
}
#[test]
fn sharded_layers_equal_the_unsharded_layer() {
for n in [2, 4] {
if ranks("sharded_layers_equal_the_unsharded_layer", n, layers).is_none() {
return;
}
}
}
struct Block {
q: Linear,
k: Linear,
v: Linear,
o: Linear,
up: Linear,
down: Linear,
}
fn attention(q: &Array, k: &Array, v: &Array, heads: i32) -> Array {
let (l, d) = (q.shape()[0], 8);
let split = |a: &Array| a.reshape([l, heads, d]).transpose([1, 0, 2]);
let (q, k, v) = (split(q), split(k), split(v));
let pos = nuvec::ops::arange(0, l, 1);
let causal = pos.reshape([l, 1]).greater_equal(pos.reshape([1, l]));
let scores = q
.matmul(&k.transpose([0, 2, 1]))
.multiply(1.0 / (d as f32).sqrt());
let scores = causal.select(&scores, f32::NEG_INFINITY);
let p = nuvec::ops::softmax(&scores, -1);
p.matmul(&v).transpose([1, 0, 2]).reshape([l, heads * d])
}
impl Block {
fn forward(&self, x: &Array) -> Array {
let a = attention(
&self.q.forward(x),
&self.k.forward(x),
&self.v.forward(x),
4,
);
let h = x.add(self.o.forward(&a));
h.add(self.down.forward(&nuvec_nn::silu(&self.up.forward(&h))))
}
fn forward_sharded(&self, x: &Array, g: &Group) -> Array {
let col = |l: &Linear| AllToShardedLinear::from_linear(l, g).unwrap();
let row = |l: &Linear| ShardedToAllLinear::from_linear(l, g).unwrap();
let heads = 4 / g.size() as i32;
let a = attention(
&col(&self.q).forward(x),
&col(&self.k).forward(x),
&col(&self.v).forward(x),
heads,
);
let h = x.add(row(&self.o).forward(&a));
h.add(row(&self.down).forward(&nuvec_nn::silu(&col(&self.up).forward(&h))))
}
}
fn transformer(g: Group) {
random::seed(11);
let block = Block {
q: Linear::new(32, 32),
k: Linear::new(32, 32),
v: Linear::new(32, 32),
o: Linear::new(32, 32),
up: Linear::new(32, 64),
down: Linear::new(64, 32),
};
let head = Linear::new(32, 50);
let x = random::normal([6, 32], DType::F32, 0.0, 1.0, None);
let want = head.forward(&block.forward(&x));
let got = head.forward(&block.forward_sharded(&x, &g));
close("block logits", &got, &want, 1e-4);
assert_eq!(
got.argmax(-1).to_vec::<u32>().unwrap(),
want.argmax(-1).to_vec::<u32>().unwrap(),
"greedy tokens"
);
}
#[test]
fn a_tensor_parallel_block_equals_the_single_process_block() {
for n in [2, 4] {
if ranks(
"a_tensor_parallel_block_equals_the_single_process_block",
n,
transformer,
)
.is_none()
{
return;
}
}
}