From 610bb1f97811af2d80d16efd96f313012beb65aa Mon Sep 17 00:00:00 2001 From: OlivierDehaene Date: Thu, 30 Mar 2023 15:26:27 +0200 Subject: [PATCH] feat(benchmark): tui based benchmarking tool (#149) --- Cargo.toml | 3 + Makefile | 3 + assets/benchmark.png | Bin 0 -> 103931 bytes benchmark/.gitignore | 1 + benchmark/Cargo.lock | 2801 +++++++++++++++++ benchmark/Cargo.toml | 35 + benchmark/README.md | 30 + benchmark/rust-toolchain.toml | 3 + benchmark/src/app.rs | 688 ++++ benchmark/src/event.rs | 65 + benchmark/src/generation.rs | 211 ++ benchmark/src/lib.rs | 110 + benchmark/src/main.rs | 119 + benchmark/src/utils.rs | 43 + proto/generate.proto | 3 + router/src/main.rs | 10 +- router/src/queue.rs | 1 + router/src/validation.rs | 1 + server/text_generation_server/cli.py | 2 +- server/text_generation_server/utils/tokens.py | 11 +- 20 files changed, 4133 insertions(+), 7 deletions(-) create mode 100644 assets/benchmark.png create mode 100644 benchmark/.gitignore create mode 100644 benchmark/Cargo.lock create mode 100644 benchmark/Cargo.toml create mode 100644 benchmark/README.md create mode 100644 benchmark/rust-toolchain.toml create mode 100644 benchmark/src/app.rs create mode 100644 benchmark/src/event.rs create mode 100644 benchmark/src/generation.rs create mode 100644 benchmark/src/lib.rs create mode 100644 benchmark/src/main.rs create mode 100644 benchmark/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index b3bd5dc..af479c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ members = [ "router/grpc-metadata", "launcher" ] +exclude = [ + "benchmark" +] [profile.release] debug = 1 diff --git a/Makefile b/Makefile index 3defd88..21fd11b 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ install-router: install-launcher: cd launcher && cargo install --path . +install-benchmark: + cd benchmark && cargo install --path . + install: install-server install-router install-launcher server-dev: diff --git a/assets/benchmark.png b/assets/benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..64d538a0598d57102b82f5984d2437f9db129da2 GIT binary patch literal 103931 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 + +# Text Generation Inference benchmarking tool + +![benchmark](../assets/benchmark.png) + + + +A lightweight benchmarking tool based inspired by [oha](https://github.com/hatoo/oha) +and powered by [tui](https://github.com/tui-rs-revival/ratatui). + +## Install + +```shell +make install-benchmark +``` + +## Run + +First, start `text-generation-inference`: + +```shell +text-generation-launcher --model-id bigscience/bloom-560m +``` + +Then run the benchmarking tool: + +```shell +text-generation-benchmark --tokenizer-name bigscience/bloom-560m +``` \ No newline at end of file diff --git a/benchmark/rust-toolchain.toml b/benchmark/rust-toolchain.toml new file mode 100644 index 0000000..dcdb06e --- /dev/null +++ b/benchmark/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.67.0" +components = ["rustfmt", "clippy"] \ No newline at end of file diff --git a/benchmark/src/app.rs b/benchmark/src/app.rs new file mode 100644 index 0000000..85026a3 --- /dev/null +++ b/benchmark/src/app.rs @@ -0,0 +1,688 @@ +/// Inspired by https://github.com/hatoo/oha/blob/bb989ea3cd77727e7743e7daa60a19894bb5e901/src/monitor.rs +use crate::generation::{Decode, Message, Prefill}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use text_generation_client::ClientError; +use tokio::sync::mpsc; +use tui::backend::Backend; +use tui::layout::{Alignment, Constraint, Direction, Layout}; +use tui::style::{Color, Modifier, Style}; +use tui::text::{Span, Spans}; +use tui::widgets::{ + Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, GraphType, Paragraph, Tabs, +}; +use tui::{symbols, Frame}; + +/// TUI powered App +pub(crate) struct App { + pub(crate) running: bool, + completed_runs: Vec, + completed_batch: usize, + current_batch: usize, + current_tab: usize, + touched_tab: bool, + zoom: bool, + is_error: bool, + data: Data, + tokenizer_name: String, + sequence_length: u32, + decode_length: u32, + n_run: usize, + batch_size: Vec, + receiver: mpsc::Receiver>, +} + +impl App { + pub(crate) fn new( + receiver: mpsc::Receiver>, + tokenizer_name: String, + sequence_length: u32, + decode_length: u32, + n_run: usize, + batch_size: Vec, + ) -> Self { + let data = Data::new(n_run, batch_size.len()); + let current_tab = 0; + + let completed_runs: Vec = (0..batch_size.len()).map(|_| 0).collect(); + let completed_batch = 0; + let current_batch = 0; + let is_error = false; + + Self { + running: true, + completed_runs, + completed_batch, + current_batch, + current_tab, + touched_tab: false, + zoom: false, + is_error, + data, + tokenizer_name, + sequence_length, + decode_length, + n_run, + batch_size, + receiver, + } + } + + /// Handle crossterm key events + pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event { + // Increase and wrap tab + KeyEvent { + code: KeyCode::Right, + .. + } + | KeyEvent { + code: KeyCode::Tab, .. + } => { + self.touched_tab = true; + self.current_tab = (self.current_tab + 1) % self.batch_size.len(); + } + // Decrease and wrap tab + KeyEvent { + code: KeyCode::Left, + .. + } => { + self.touched_tab = true; + if self.current_tab > 0 { + self.current_tab -= 1; + } else { + self.current_tab = self.batch_size.len() - 1; + } + } + // Zoom on throughput/latency fig + KeyEvent { + code: KeyCode::Char('+'), + .. + } => { + self.zoom = true; + } + // Unzoom on throughput/latency fig + KeyEvent { + code: KeyCode::Char('-'), + .. + } => { + self.zoom = false; + } + // Quit + KeyEvent { + code: KeyCode::Char('q'), + .. + } + | KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + } => { + self.running = false; + } + _ => (), + } + } + + /// Get all pending messages from generation task + pub(crate) fn tick(&mut self) { + while let Ok(message) = self.receiver.try_recv() { + match message { + Ok(message) => match message { + Message::Prefill(step) => self.data.push_prefill(step, self.current_batch), + Message::Decode(step) => self.data.push_decode(step, self.current_batch), + Message::EndRun => { + self.completed_runs[self.current_batch] += 1; + } + Message::EndBatch => { + self.data.end_batch(self.current_batch); + self.completed_batch += 1; + + if self.current_batch < self.batch_size.len() - 1 { + // Only go to next tab if the user never touched the tab keys + if !self.touched_tab { + self.current_tab += 1; + } + + self.current_batch += 1; + } + } + Message::Warmup => {} + }, + Err(_) => self.is_error = true, + } + } + } + + /// Render frame + pub fn render(&mut self, f: &mut Frame<'_, B>) { + let batch_progress = + (self.completed_batch as f64 / self.batch_size.len() as f64).clamp(0.0, 1.0); + let run_progress = + (self.completed_runs[self.current_batch] as f64 / self.n_run as f64).clamp(0.0, 1.0); + + // Vertical layout + let row5 = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(13), + Constraint::Min(10), + ] + .as_ref(), + ) + .split(f.size()); + + // Top row horizontal layout + let top = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(row5[2]); + + // Mid row horizontal layout + let mid = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(row5[3]); + + // Left mid row vertical layout + let prefill_text = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(8), Constraint::Length(5)].as_ref()) + .split(mid[0]); + + // Right mid row vertical layout + let decode_text = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(8), Constraint::Length(5)].as_ref()) + .split(mid[2]); + let decode_text_latency = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(decode_text[0]); + + // Bottom row horizontal layout + let bottom = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(row5[4]); + + // Title + let title = Block::default() + .borders(Borders::NONE) + .title(format!( + "Model: {} | Sequence Length: {} | Decode Length: {}", + self.tokenizer_name, self.sequence_length, self.decode_length + )) + .style( + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::White), + ); + f.render_widget(title, row5[0]); + + // Helper + let helper = Block::default() + .borders(Borders::NONE) + .title("<- | tab | ->: change batch tab | q / CTRL + c: quit | +/-: zoom") + .title_alignment(Alignment::Right) + .style(Style::default().fg(Color::White)); + f.render_widget(helper, row5[0]); + + // Batch tabs + let titles = self + .batch_size + .iter() + .map(|b| { + Spans::from(vec![Span::styled( + format!("Batch: {b}"), + Style::default().fg(Color::White), + )]) + }) + .collect(); + let tabs = Tabs::new(titles) + .block(Block::default().borders(Borders::ALL).title("Tabs")) + .select(self.current_tab) + .style(Style::default().fg(Color::LightCyan)) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::Black), + ); + f.render_widget(tabs, row5[1]); + + // Total progress bar + let color = if self.is_error { + Color::Red + } else { + Color::LightGreen + }; + let batch_gauge = progress_gauge( + "Total Progress", + format!("{} / {}", self.completed_batch, self.batch_size.len()), + batch_progress, + color, + ); + f.render_widget(batch_gauge, top[0]); + + // Batch progress Bar + let color = if self.is_error { + Color::Red + } else { + Color::LightBlue + }; + let run_gauge = progress_gauge( + "Batch Progress", + format!( + "{} / {}", + self.completed_runs[self.current_batch], self.n_run + ), + run_progress, + color, + ); + f.render_widget(run_gauge, top[1]); + + // Prefill text infos + let prefill_latency_block = latency_paragraph( + &mut self.data.prefill_latencies[self.current_tab], + "Prefill", + ); + let prefill_throughput_block = + throughput_paragraph(&self.data.prefill_throughputs[self.current_tab], "Prefill"); + + f.render_widget(prefill_latency_block, prefill_text[0]); + f.render_widget(prefill_throughput_block, prefill_text[1]); + + // Prefill latency histogram + let histo_width = 7; + let bins = if mid[1].width < 2 { + 0 + } else { + (mid[1].width as usize - 2) / (histo_width + 1) + } + .max(2); + + let histo_data = + latency_histogram_data(&self.data.prefill_latencies[self.current_tab], bins); + let histo_data_str: Vec<(&str, u64)> = + histo_data.iter().map(|(l, v)| (l.as_str(), *v)).collect(); + let prefill_histogram = + latency_histogram(&histo_data_str, "Prefill").bar_width(histo_width as u16); + f.render_widget(prefill_histogram, mid[1]); + + // Decode text info + let decode_latency_block = latency_paragraph( + &mut self.data.decode_latencies[self.current_tab], + "Decode Total", + ); + let decode_token_latency_block = latency_paragraph( + &mut self.data.decode_token_latencies[self.current_tab], + "Decode Token", + ); + let decode_throughput_block = + throughput_paragraph(&self.data.decode_throughputs[self.current_tab], "Decode"); + f.render_widget(decode_latency_block, decode_text_latency[0]); + f.render_widget(decode_token_latency_block, decode_text_latency[1]); + f.render_widget(decode_throughput_block, decode_text[1]); + + // Decode latency histogram + let histo_data = + latency_histogram_data(&self.data.decode_latencies[self.current_tab], bins); + let histo_data_str: Vec<(&str, u64)> = + histo_data.iter().map(|(l, v)| (l.as_str(), *v)).collect(); + let decode_histogram = + latency_histogram(&histo_data_str, "Decode").bar_width(histo_width as u16); + f.render_widget(decode_histogram, mid[3]); + + // Prefill latency/throughput chart + let prefill_latency_throughput_chart = latency_throughput_chart( + &self.data.prefill_batch_latency_throughput, + &self.batch_size, + self.zoom, + "Prefill", + ); + f.render_widget(prefill_latency_throughput_chart, bottom[0]); + + // Decode latency/throughput chart + let decode_latency_throughput_chart = latency_throughput_chart( + &self.data.decode_batch_latency_throughput, + &self.batch_size, + self.zoom, + "Decode", + ); + f.render_widget(decode_latency_throughput_chart, bottom[1]); + } +} + +/// App internal data struct +struct Data { + prefill_latencies: Vec>, + prefill_throughputs: Vec>, + decode_latencies: Vec>, + decode_token_latencies: Vec>, + decode_throughputs: Vec>, + prefill_batch_latency_throughput: Vec<(f64, f64)>, + decode_batch_latency_throughput: Vec<(f64, f64)>, +} + +impl Data { + fn new(n_run: usize, n_batch: usize) -> Self { + let prefill_latencies: Vec> = + (0..n_batch).map(|_| Vec::with_capacity(n_run)).collect(); + let prefill_throughputs: Vec> = prefill_latencies.clone(); + + let decode_latencies: Vec> = prefill_latencies.clone(); + let decode_token_latencies: Vec> = decode_latencies.clone(); + let decode_throughputs: Vec> = prefill_throughputs.clone(); + + let prefill_batch_latency_throughput: Vec<(f64, f64)> = Vec::with_capacity(n_batch); + let decode_batch_latency_throughput: Vec<(f64, f64)> = + prefill_batch_latency_throughput.clone(); + + Self { + prefill_latencies, + prefill_throughputs, + decode_latencies, + decode_token_latencies, + decode_throughputs, + prefill_batch_latency_throughput, + decode_batch_latency_throughput, + } + } + + fn push_prefill(&mut self, prefill: Prefill, batch_idx: usize) { + let latency = prefill.latency.as_millis() as f64; + self.prefill_latencies[batch_idx].push(latency); + self.prefill_throughputs[batch_idx].push(prefill.throughput); + } + + fn push_decode(&mut self, decode: Decode, batch_idx: usize) { + let latency = decode.latency.as_millis() as f64; + let token_latency = decode.token_latency.as_millis() as f64; + self.decode_latencies[batch_idx].push(latency); + self.decode_token_latencies[batch_idx].push(token_latency); + self.decode_throughputs[batch_idx].push(decode.throughput); + } + + fn end_batch(&mut self, batch_idx: usize) { + self.prefill_batch_latency_throughput.push(( + self.prefill_latencies[batch_idx].iter().sum::() + / self.prefill_latencies[batch_idx].len() as f64, + self.prefill_throughputs[batch_idx].iter().sum::() + / self.prefill_throughputs[batch_idx].len() as f64, + )); + self.decode_batch_latency_throughput.push(( + self.decode_latencies[batch_idx].iter().sum::() + / self.decode_latencies[batch_idx].len() as f64, + self.decode_throughputs[batch_idx].iter().sum::() + / self.decode_throughputs[batch_idx].len() as f64, + )); + } +} + +/// Progress bar +fn progress_gauge(title: &str, label: String, progress: f64, color: Color) -> Gauge { + Gauge::default() + .block(Block::default().title(title).borders(Borders::ALL)) + .gauge_style(Style::default().fg(color)) + .label(Span::raw(label)) + .ratio(progress) +} + +/// Throughput paragraph +fn throughput_paragraph<'a>(throughput: &Vec, name: &'static str) -> Paragraph<'a> { + // Throughput average/high/low texts + let throughput_texts = statis_spans(throughput, "tokens/secs"); + + // Throughput block + Paragraph::new(throughput_texts).block( + Block::default() + .title(Span::raw(format!("{name} Throughput"))) + .borders(Borders::ALL), + ) +} + +/// Latency paragraph +fn latency_paragraph<'a>(latency: &mut Vec, name: &'static str) -> Paragraph<'a> { + // Latency average/high/low texts + let mut latency_texts = statis_spans(latency, "ms"); + + // Sort latency for percentiles + float_ord::sort(latency); + let latency_percentiles = crate::utils::percentiles(latency, &[50, 90, 99]); + + // Latency p50/p90/p99 texts + let colors = vec![Color::LightGreen, Color::LightYellow, Color::LightRed]; + for (i, (name, value)) in latency_percentiles.iter().enumerate() { + let span = Spans::from(vec![Span::styled( + format!("{name}: {value:.2} ms"), + Style::default().fg(colors[i]), + )]); + latency_texts.push(span); + } + + Paragraph::new(latency_texts).block( + Block::default() + .title(Span::raw(format!("{name} Latency"))) + .borders(Borders::ALL), + ) +} + +/// Average/High/Low spans +fn statis_spans<'a>(data: &Vec, unit: &'static str) -> Vec> { + vec![ + Spans::from(vec![Span::styled( + format!( + "Average: {:.2} {unit}", + data.iter().sum::() / data.len() as f64 + ), + Style::default().fg(Color::LightBlue), + )]), + Spans::from(vec![Span::styled( + format!( + "Lowest: {:.2} {unit}", + data.iter() + .min_by(|a, b| a.total_cmp(b)) + .unwrap_or(&std::f64::NAN) + ), + Style::default().fg(Color::Reset), + )]), + Spans::from(vec![Span::styled( + format!( + "Highest: {:.2} {unit}", + data.iter() + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(&std::f64::NAN) + ), + Style::default().fg(Color::Reset), + )]), + ] +} + +/// Latency histogram data +fn latency_histogram_data(latency: &[f64], bins: usize) -> Vec<(String, u64)> { + let histo_data: Vec<(String, u64)> = { + let histo = crate::utils::histogram(latency, bins); + histo + .into_iter() + .map(|(label, v)| (format!("{label:.2}"), v as u64)) + .collect() + }; + + histo_data +} + +/// Latency Histogram +fn latency_histogram<'a>( + histo_data_str: &'a Vec<(&'a str, u64)>, + name: &'static str, +) -> BarChart<'a> { + BarChart::default() + .block( + Block::default() + .title(format!("{name} latency histogram")) + .style(Style::default().fg(Color::LightYellow).bg(Color::Reset)) + .borders(Borders::ALL), + ) + .data(histo_data_str.as_slice()) +} + +/// Latency/Throughput chart +fn latency_throughput_chart<'a>( + latency_throughput: &'a Vec<(f64, f64)>, + batch_sizes: &'a [u32], + zoom: bool, + name: &'static str, +) -> Chart<'a> { + let latency_iter = latency_throughput.iter().map(|(l, _)| l); + let throughput_iter = latency_throughput.iter().map(|(_, t)| t); + + // Get extreme values + let min_latency: f64 = *latency_iter + .clone() + .min_by(|a, b| a.total_cmp(b)) + .unwrap_or(&std::f64::NAN); + let max_latency: f64 = *latency_iter + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(&std::f64::NAN); + let min_throughput: f64 = *throughput_iter + .clone() + .min_by(|a, b| a.total_cmp(b)) + .unwrap_or(&std::f64::NAN); + let max_throughput: f64 = *throughput_iter + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(&std::f64::NAN); + + // Char min max values + let min_x = if zoom { + ((min_latency - 0.05 * min_latency) / 100.0).floor() * 100.0 + } else { + 0.0 + }; + let max_x = ((max_latency + 0.05 * max_latency) / 100.0).ceil() * 100.0; + let step_x = (max_x - min_x) / 4.0; + + // Chart min max values + let min_y = if zoom { + ((min_throughput - 0.05 * min_throughput) / 100.0).floor() * 100.0 + } else { + 0.0 + }; + let max_y = ((max_throughput + 0.05 * max_throughput) / 100.0).ceil() * 100.0; + let step_y = (max_y - min_y) / 4.0; + + // Labels + let mut x_labels = vec![Span::styled( + format!("{min_x:.2}"), + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Gray) + .bg(Color::Reset), + )]; + for i in 0..3 { + x_labels.push(Span::styled( + format!("{:.2}", min_x + ((i + 1) as f64 * step_x)), + Style::default().fg(Color::Gray).bg(Color::Reset), + )); + } + x_labels.push(Span::styled( + format!("{max_x:.2}"), + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Gray) + .bg(Color::Reset), + )); + + // Labels + let mut y_labels = vec![Span::styled( + format!("{min_y:.2}"), + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Gray) + .bg(Color::Reset), + )]; + for i in 0..3 { + y_labels.push(Span::styled( + format!("{:.2}", min_y + ((i + 1) as f64 * step_y)), + Style::default().fg(Color::Gray).bg(Color::Reset), + )); + } + y_labels.push(Span::styled( + format!("{max_y:.2}"), + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Gray) + .bg(Color::Reset), + )); + + // Chart dataset + let colors = color_vec(); + let datasets: Vec = (0..latency_throughput.len()) + .map(|i| { + let color_idx = i % colors.len(); + + Dataset::default() + .name(batch_sizes[i].to_string()) + .marker(symbols::Marker::Block) + .style(Style::default().fg(colors[color_idx])) + .graph_type(GraphType::Scatter) + .data(&latency_throughput[i..(i + 1)]) + }) + .collect(); + + // Chart + Chart::new(datasets) + .style(Style::default().fg(Color::Cyan).bg(Color::Reset)) + .block( + Block::default() + .title(Span::styled( + format!("{name} throughput over latency"), + Style::default().fg(Color::Gray).bg(Color::Reset), + )) + .borders(Borders::ALL), + ) + .x_axis( + Axis::default() + .title("ms") + .style(Style::default().fg(Color::Gray).bg(Color::Reset)) + .labels(x_labels) + .bounds([min_x, max_x]), + ) + .y_axis( + Axis::default() + .title("tokens/secs") + .style(Style::default().fg(Color::Gray).bg(Color::Reset)) + .labels(y_labels) + .bounds([min_y, max_y]), + ) +} + +// Colors for latency/throughput chart +fn color_vec() -> Vec { + vec![ + Color::Red, + Color::Green, + Color::Yellow, + Color::Blue, + Color::Magenta, + Color::Cyan, + Color::Gray, + Color::DarkGray, + Color::LightRed, + Color::LightGreen, + Color::LightYellow, + Color::LightBlue, + Color::LightMagenta, + Color::LightCyan, + ] +} diff --git a/benchmark/src/event.rs b/benchmark/src/event.rs new file mode 100644 index 0000000..91ce840 --- /dev/null +++ b/benchmark/src/event.rs @@ -0,0 +1,65 @@ +/// Inspired by https://github.com/orhun/rust-tui-template/blob/472aa515119d4c94903eac12d9784417281dc7f5/src/event.rs +use crossterm::event; +use std::time::{Duration, Instant}; +use tokio::sync::{broadcast, mpsc}; + +/// Events +#[derive(Debug)] +pub(crate) enum Event { + /// Terminal tick. + Tick, + /// Key press. + Key(event::KeyEvent), + /// Terminal resize. + Resize(u16, u16), +} + +pub(crate) async fn terminal_event_task( + fps: u32, + event_sender: mpsc::Sender, + mut shutdown_receiver: broadcast::Receiver<()>, + _shutdown_guard_sender: mpsc::Sender<()>, +) { + // End task if a message is received on shutdown_receiver + // _shutdown_guard_sender will be dropped once the task is finished + tokio::select! { + _ = event_loop(fps, event_sender) => { + }, + _ = shutdown_receiver.recv() => {} + } +} + +/// Main event loop +async fn event_loop(fps: u32, event_sender: mpsc::Sender) { + // Frame budget + let per_frame = Duration::from_secs(1) / fps; + + // When was last frame executed + let mut last_frame = Instant::now(); + + loop { + // Sleep to avoid blocking the thread for too long + if let Some(sleep) = per_frame.checked_sub(last_frame.elapsed()) { + tokio::time::sleep(sleep).await; + } + + // Get crossterm event and send a new one over the channel + if event::poll(Duration::from_secs(0)).expect("no events available") { + match event::read().expect("unable to read event") { + event::Event::Key(e) => event_sender.send(Event::Key(e)).await.unwrap_or(()), + event::Event::Resize(w, h) => { + event_sender.send(Event::Resize(w, h)).await.unwrap_or(()) + } + _ => (), + } + } + + // Frame budget exceeded + if last_frame.elapsed() >= per_frame { + // Send tick + event_sender.send(Event::Tick).await.unwrap_or(()); + // Rest last_frame time + last_frame = Instant::now(); + } + } +} diff --git a/benchmark/src/generation.rs b/benchmark/src/generation.rs new file mode 100644 index 0000000..3a6316a --- /dev/null +++ b/benchmark/src/generation.rs @@ -0,0 +1,211 @@ +use std::time::{Duration, Instant}; +use text_generation_client::{ + Batch, ClientError, NextTokenChooserParameters, Request, ShardedClient, + StoppingCriteriaParameters, +}; +use tokenizers::{Tokenizer, TruncationDirection}; +use tokio::sync::{broadcast, mpsc}; + +const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + +#[derive(Debug, Clone)] +pub(crate) struct Prefill { + pub(crate) latency: Duration, + pub(crate) throughput: f64, +} + +#[derive(Debug, Clone)] +pub(crate) struct Decode { + pub(crate) latency: Duration, + pub(crate) token_latency: Duration, + pub(crate) throughput: f64, +} + +#[derive(Debug)] +pub(crate) enum Message { + Warmup, + Prefill(Prefill), + Decode(Decode), + EndRun, + EndBatch, +} + +/// Benchmarking task +#[allow(clippy::too_many_arguments)] +pub(crate) async fn generation_task( + tokenizer: Tokenizer, + batch_size: Vec, + sequence_length: u32, + decode_length: u32, + n_runs: usize, + warmups: usize, + client: ShardedClient, + run_sender: mpsc::Sender>, + mut shutdown_receiver: broadcast::Receiver<()>, + _shutdown_guard_sender: mpsc::Sender<()>, +) { + // End task if a message is received on shutdown_receiver + // _shutdown_guard_sender will be dropped once the task is finished + tokio::select! { + res = generate_runs(tokenizer, batch_size, sequence_length, decode_length, n_runs, warmups, client, run_sender.clone()) => { + if let Err(err) = res { + run_sender.send(Err(err)).await.unwrap_or(()); + } + }, + _ = shutdown_receiver.recv() => {} + } +} + +/// Benchmark prefill/decode +#[allow(clippy::too_many_arguments)] +async fn generate_runs( + tokenizer: Tokenizer, + batch_size: Vec, + sequence_length: u32, + decode_length: u32, + n_runs: usize, + warmups: usize, + mut client: ShardedClient, + run_sender: mpsc::Sender>, +) -> Result<(), ClientError> { + // Create a dummy sequence + let sequence = create_sequence(sequence_length, tokenizer); + + for b in batch_size { + // Warmups on batch size + for _ in 0..warmups { + let (_, decode_batch) = + prefill(sequence.clone(), b, decode_length, &mut client).await?; + let _ = decode(decode_batch, &mut client).await?; + // Send warmup message + run_sender.send(Ok(Message::Warmup)).await.unwrap_or(()); + } + + for _ in 0..n_runs { + let (prefill, decode_batch) = + prefill(sequence.clone(), b, decode_length, &mut client).await?; + // Send prefill message + run_sender + .send(Ok(Message::Prefill(prefill))) + .await + .unwrap_or(()); + + let decode = decode(decode_batch, &mut client).await?; + + // Send decode message + run_sender + .send(Ok(Message::Decode(decode))) + .await + .unwrap_or(()); + + // Send run ended message + run_sender.send(Ok(Message::EndRun)).await.unwrap_or(()); + } + // Batch ended + run_sender.send(Ok(Message::EndBatch)).await.unwrap_or(()); + } + Ok(()) +} + +// Run a prefill step +async fn prefill( + sequence: String, + batch_size: u32, + decode_length: u32, + client: &mut ShardedClient, +) -> Result<(Prefill, Batch), ClientError> { + // Create requests + let requests = (0..batch_size) + .map(|id| Request { + id: id.into(), + inputs: sequence.clone(), + parameters: Some(NextTokenChooserParameters { + temperature: 1.0, + top_k: 0, + top_p: 1.0, + typical_p: 1.0, + do_sample: false, + seed: 0, + repetition_penalty: 1.0, + watermark: false, + }), + stopping_parameters: Some(StoppingCriteriaParameters { + max_new_tokens: decode_length, + stop_sequences: vec![], + ignore_eos_token: true, // Will not stop even if a eos token is generated + }), + }) + .collect(); + + let batch = Batch { + id: 0, + requests, + size: batch_size, + }; + + // Run prefill + let start_time = Instant::now(); + let (_, decode_batch) = client.prefill(batch.clone()).await?; + + // Get latency + let latency = start_time.elapsed(); + + // Compute throughput from latency and batch size + let throughput = batch_size as f64 / latency.as_secs_f64(); + + // Decode batch cannot be empty + let decode_batch = decode_batch.expect("decode_batch is None. This is a bug."); + + let step = Prefill { + latency, + throughput, + }; + + Ok((step, decode_batch)) +} + +/// Run a full decode +async fn decode(batch: Batch, client: &mut ShardedClient) -> Result { + let mut decode_length = 0; + let batch_size = batch.size; + + let start_time = Instant::now(); + + // Full decode over decode length + let mut next_batch = Some(batch); + while let Some(batch) = next_batch { + let result = client.decode(vec![batch]).await?; + next_batch = result.1; + decode_length += 1; + } + + // Get latency + let latency = start_time.elapsed(); + let token_latency = latency / decode_length; + + // Compute throughput from latency, batch size and decode length + let throughput = (batch_size * decode_length) as f64 / latency.as_secs_f64(); + + let step = Decode { + latency, + token_latency, + throughput, + }; + Ok(step) +} + +/// Create a dummy sequence of the correct length +fn create_sequence(sequence_length: u32, tokenizer: Tokenizer) -> String { + let lorem_ipsum_length = tokenizer.encode(LOREM_IPSUM, true).unwrap().len(); + // Repeat lorem ipsum to cover sequence length + let string_sequence = + LOREM_IPSUM.repeat((0..sequence_length).step_by(lorem_ipsum_length).len()); + // Encode sequence + let mut encoding = tokenizer.encode(string_sequence, true).unwrap(); + // Truncate to sequence_length + encoding.truncate(sequence_length as usize, 0, TruncationDirection::Left); + // Decode + tokenizer + .decode(Vec::from(encoding.get_ids()), false) + .unwrap() +} diff --git a/benchmark/src/lib.rs b/benchmark/src/lib.rs new file mode 100644 index 0000000..4da0b57 --- /dev/null +++ b/benchmark/src/lib.rs @@ -0,0 +1,110 @@ +mod app; +mod event; +mod generation; +mod utils; + +use crate::app::App; +use crate::event::Event; +use crossterm::ExecutableCommand; +use std::io; +use text_generation_client::ShardedClient; +use tokenizers::Tokenizer; +use tokio::sync::{broadcast, mpsc}; +use tui::backend::CrosstermBackend; +use tui::Terminal; + +/// Run benchmarking app +#[allow(clippy::too_many_arguments)] +pub async fn run( + tokenizer_name: String, + tokenizer: Tokenizer, + batch_size: Vec, + sequence_length: u32, + decode_length: u32, + n_runs: usize, + warmups: usize, + client: ShardedClient, +) -> Result<(), crossterm::ErrorKind> { + // Initialize terminal properties + crossterm::terminal::enable_raw_mode()?; + io::stdout().execute(crossterm::terminal::EnterAlternateScreen)?; + io::stdout().execute(crossterm::cursor::Hide)?; + + // Initialize terminal + let mut terminal = { + let backend = CrosstermBackend::new(io::stdout()); + Terminal::new(backend)? + }; + + // Create message channel between generation_task and app + let (run_sender, run_receiver) = mpsc::channel(8); + // Crossterm event channel + let (event_sender, mut event_receiver) = mpsc::channel(8); + // Shutdown channel to terminate tasks + let (shutdown_sender, _) = broadcast::channel(1); + // Channel to check if tasks terminated + let (shutdown_guard_sender, mut shutdown_guard_receiver) = mpsc::channel(1); + + // Create generation task + tokio::spawn(generation::generation_task( + tokenizer, + batch_size.clone(), + sequence_length, + decode_length, + n_runs, + warmups, + client, + run_sender, + shutdown_sender.subscribe(), + shutdown_guard_sender.clone(), + )); + + // Create event task + tokio::spawn(event::terminal_event_task( + 250, + event_sender, + shutdown_sender.subscribe(), + shutdown_guard_sender.clone(), + )); + + // Drop our end of shutdown sender + drop(shutdown_guard_sender); + + // Create App + let mut app = App::new( + run_receiver, + tokenizer_name, + sequence_length, + decode_length, + n_runs, + batch_size, + ); + + while app.running { + // Draw frame + terminal.draw(|frame| app.render(frame))?; + + // Await a new event from event handling task + match event_receiver.recv().await { + None => break, + // Update app state + Some(event) => match event { + Event::Tick => app.tick(), + Event::Key(key_event) => app.handle_key_event(key_event), + _ => {} + }, + } + } + + // Ask tasks to shutdown + let _ = shutdown_sender.send(()); + // Wait for tasks to shutdown + let _ = shutdown_guard_receiver.recv().await; + + // Revert terminal to original view + io::stdout().execute(crossterm::terminal::LeaveAlternateScreen)?; + crossterm::terminal::disable_raw_mode()?; + io::stdout().execute(crossterm::cursor::Show)?; + + Ok(()) +} diff --git a/benchmark/src/main.rs b/benchmark/src/main.rs new file mode 100644 index 0000000..443af77 --- /dev/null +++ b/benchmark/src/main.rs @@ -0,0 +1,119 @@ +/// Text Generation Inference benchmarking tool +/// +/// Inspired by the great Oha app: https://github.com/hatoo/oha +/// and: https://github.com/orhun/rust-tui-template +use clap::Parser; +use std::path::Path; +use text_generation_client::ShardedClient; +use tokenizers::Tokenizer; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +/// App Configuration +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(short, long, env)] + tokenizer_name: String, + #[clap(short, long)] + batch_size: Option>, + #[clap(default_value = "10", short, long, env)] + sequence_length: u32, + #[clap(default_value = "8", short, long, env)] + decode_length: u32, + #[clap(default_value = "10", short, long, env)] + runs: usize, + #[clap(default_value = "1", short, long, env)] + warmups: usize, + #[clap(default_value = "/tmp/text-generation-server-0", short, long, env)] + master_shard_uds_path: String, +} + +fn main() -> Result<(), Box> { + // Get args + let args = Args::parse(); + // Pattern match configuration + let Args { + tokenizer_name, + batch_size, + sequence_length, + decode_length, + runs, + warmups, + master_shard_uds_path, + } = args; + + let batch_size = batch_size.unwrap_or(vec![1, 2, 4, 8, 16, 32]); + + init_logging(); + + // Tokenizer instance + // This will only be used to validate payloads + tracing::info!("Loading tokenizer"); + let local_path = Path::new(&tokenizer_name); + let tokenizer = + if local_path.exists() && local_path.is_dir() && local_path.join("tokenizer.json").exists() + { + // Load local tokenizer + tracing::info!("Found local tokenizer"); + Tokenizer::from_file(local_path.join("tokenizer.json")).unwrap() + } else { + // Download and instantiate tokenizer + // We need to download it outside of the Tokio runtime + tracing::info!("Downloading tokenizer"); + Tokenizer::from_pretrained(tokenizer_name.clone(), None).unwrap() + }; + tracing::info!("Tokenizer loaded"); + + // Launch Tokio runtime + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + // Instantiate sharded client from the master unix socket + tracing::info!("Connect to model server"); + let mut sharded_client = ShardedClient::connect_uds(master_shard_uds_path) + .await + .expect("Could not connect to server"); + // Clear the cache; useful if the webserver rebooted + sharded_client + .clear_cache(None) + .await + .expect("Unable to clear cache"); + tracing::info!("Connected"); + + // Run app + text_generation_benchmark::run( + tokenizer_name, + tokenizer, + batch_size, + sequence_length, + decode_length, + runs, + warmups, + sharded_client, + ) + .await + .unwrap(); + }); + Ok(()) +} + +/// Init logging using LOG_LEVEL +fn init_logging() { + // STDOUT/STDERR layer + let fmt_layer = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true); + + // Filter events with LOG_LEVEL + let env_filter = + EnvFilter::try_from_env("LOG_LEVEL").unwrap_or_else(|_| EnvFilter::new("info")); + + tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer) + .init(); +} diff --git a/benchmark/src/utils.rs b/benchmark/src/utils.rs new file mode 100644 index 0000000..d096d65 --- /dev/null +++ b/benchmark/src/utils.rs @@ -0,0 +1,43 @@ +/// MIT License +// +// Copyright (c) 2020 hatoo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +use std::collections::BTreeMap; + +pub(crate) fn histogram(values: &[f64], bins: usize) -> Vec<(f64, usize)> { + assert!(bins >= 2); + let mut bucket: Vec = vec![0; bins]; + let min = values.iter().collect::().min(); + let max = values.iter().collect::().max(); + let step = (max - min) / (bins - 1) as f64; + + for &v in values { + let i = std::cmp::min(((v - min) / step).ceil() as usize, bins - 1); + bucket[i] += 1; + } + + bucket + .into_iter() + .enumerate() + .map(|(i, v)| (min + step * i as f64, v)) + .collect() +} + +pub(crate) fn percentiles(values: &[f64], pecents: &[i32]) -> BTreeMap { + pecents + .iter() + .map(|&p| { + let i = (f64::from(p) / 100.0 * values.len() as f64) as usize; + (format!("p{p}"), *values.get(i).unwrap_or(&std::f64::NAN)) + }) + .collect() +} diff --git a/proto/generate.proto b/proto/generate.proto index 0bac435..8639302 100644 --- a/proto/generate.proto +++ b/proto/generate.proto @@ -53,6 +53,9 @@ message StoppingCriteriaParameters { uint32 max_new_tokens = 1; /// Optional stopping sequences repeated string stop_sequences = 2; + /// Ignore end of sequence token + /// used for benchmarking + bool ignore_eos_token = 3; } message Request { diff --git a/router/src/main.rs b/router/src/main.rs index 81c6aee..f602857 100644 --- a/router/src/main.rs +++ b/router/src/main.rs @@ -37,7 +37,7 @@ struct Args { max_waiting_tokens: usize, #[clap(default_value = "3000", long, short, env)] port: u16, - #[clap(default_value = "/tmp/text-generation-0", long, env)] + #[clap(default_value = "/tmp/text-generation-server-0", long, env)] master_shard_uds_path: String, #[clap(default_value = "bigscience/bloom", long, env)] tokenizer_name: String, @@ -76,6 +76,8 @@ fn main() -> Result<(), std::io::Error> { panic!("validation_workers must be > 0"); } + init_logging(otlp_endpoint, json_output); + // CORS allowed origins // map to go inside the option and then map to parse from String to HeaderValue // Finally, convert to AllowOrigin @@ -89,17 +91,21 @@ fn main() -> Result<(), std::io::Error> { // Tokenizer instance // This will only be used to validate payloads + tracing::info!("Loading tokenizer"); let local_path = Path::new(&tokenizer_name); let tokenizer = if local_path.exists() && local_path.is_dir() && local_path.join("tokenizer.json").exists() { // Load local tokenizer + tracing::info!("Found local tokenizer"); Tokenizer::from_file(local_path.join("tokenizer.json")).unwrap() } else { // Download and instantiate tokenizer // We need to download it outside of the Tokio runtime + tracing::info!("Downloading tokenizer"); Tokenizer::from_pretrained(tokenizer_name.clone(), None).unwrap() }; + tracing::info!("Tokenizer loaded"); // Launch Tokio runtime tokio::runtime::Builder::new_multi_thread() @@ -107,8 +113,6 @@ fn main() -> Result<(), std::io::Error> { .build() .unwrap() .block_on(async { - init_logging(otlp_endpoint, json_output); - // Get pipeline tag let model_info = reqwest::get(format!( "https://huggingface.co/api/models/{tokenizer_name}" diff --git a/router/src/queue.rs b/router/src/queue.rs index df2087e..2899ccd 100644 --- a/router/src/queue.rs +++ b/router/src/queue.rs @@ -237,6 +237,7 @@ mod tests { watermark: false, }, stopping_parameters: StoppingCriteriaParameters { + ignore_eos_token: false, max_new_tokens: 0, stop_sequences: vec![], }, diff --git a/router/src/validation.rs b/router/src/validation.rs index 1c350ca..ec67cef 100644 --- a/router/src/validation.rs +++ b/router/src/validation.rs @@ -315,6 +315,7 @@ fn validate( let stopping_parameters = StoppingCriteriaParameters { max_new_tokens, stop_sequences, + ignore_eos_token: false, }; metrics::histogram!("tgi_request_input_length", input_length as f64); diff --git a/server/text_generation_server/cli.py b/server/text_generation_server/cli.py index 6308ef6..4d48f49 100644 --- a/server/text_generation_server/cli.py +++ b/server/text_generation_server/cli.py @@ -18,7 +18,7 @@ def serve( revision: Optional[str] = None, sharded: bool = False, quantize: bool = False, - uds_path: Path = "/tmp/text-generation", + uds_path: Path = "/tmp/text-generation-server", logger_level: str = "INFO", json_output: bool = False, otlp_endpoint: Optional[str] = None, diff --git a/server/text_generation_server/utils/tokens.py b/server/text_generation_server/utils/tokens.py index 597fbe7..b923857 100644 --- a/server/text_generation_server/utils/tokens.py +++ b/server/text_generation_server/utils/tokens.py @@ -123,20 +123,22 @@ class StoppingCriteria: self, eos_token_id: int, stop_sequence_criterias: List[StopSequenceCriteria], - max_new_tokens=20, + max_new_tokens: int = 20, + ignore_eos_token: bool = False, ): self.eos_token_id = eos_token_id self.stop_sequence_criterias = stop_sequence_criterias self.max_new_tokens = max_new_tokens self.current_tokens = 0 self.current_output = "" + self.ignore_eos_token = ignore_eos_token def __call__(self, last_token: int, last_output: str) -> Tuple[bool, Optional[str]]: self.current_tokens += 1 if self.current_tokens >= self.max_new_tokens: return True, FinishReason.FINISH_REASON_LENGTH - if last_token == self.eos_token_id: + if not self.ignore_eos_token and last_token == self.eos_token_id: return True, FinishReason.FINISH_REASON_EOS_TOKEN self.current_output += last_output @@ -156,5 +158,8 @@ class StoppingCriteria: StopSequenceCriteria(sequence) for sequence in pb.stop_sequences ] return StoppingCriteria( - tokenizer.eos_token_id, stop_sequence_criterias, pb.max_new_tokens + tokenizer.eos_token_id, + stop_sequence_criterias, + pb.max_new_tokens, + pb.ignore_eos_token, )