From 157bcf2286a4004ebf89e107bbe5ad124ae3714c Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sun, 18 Feb 2024 17:13:04 +0100 Subject: [PATCH 01/98] server: init functional test --- .github/workflows/server-test.yml | 70 +++++++++++++++++++ examples/server/tests/README.md | 9 +++ .../server/tests/features/completions.feature | 11 +++ examples/server/tests/features/oai.feature | 13 ++++ .../server/tests/features/steps/completion.py | 24 +++++++ examples/server/tests/features/steps/oai.py | 44 ++++++++++++ examples/server/tests/requirements.txt | 2 + 7 files changed, 173 insertions(+) create mode 100644 .github/workflows/server-test.yml create mode 100644 examples/server/tests/README.md create mode 100644 examples/server/tests/features/completions.feature create mode 100644 examples/server/tests/features/oai.feature create mode 100644 examples/server/tests/features/steps/completion.py create mode 100644 examples/server/tests/features/steps/oai.py create mode 100644 examples/server/tests/requirements.txt diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml new file mode 100644 index 0000000000000..d47c593f35c66 --- /dev/null +++ b/.github/workflows/server-test.yml @@ -0,0 +1,70 @@ +# Server test scenario +name: Server Integration Tests + +# FIXME put only necessary triggers +on: + push: + branches: + - master + - test/server-add-ci-test # FIXME remove + paths: ['.github/workflows/server-test.yml', '**/CMakeLists.txt', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', 'examples/server/**.*'] + +jobs: + ubuntu-latest-cmake: + runs-on: ubuntu-latest + + steps: + - name: Clone + id: checkout + uses: actions/checkout@v3 + + - name: Dependencies + id: depends + run: | + sudo apt-get update + sudo apt-get install build-essential + + - name: Build + id: cmake_build + run: | + mkdir build + cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + cmake --build . --config Release -j $(nproc) + + - name: Tests dependencies + id: test_dependencies + run: | + pip install -r examples/server/tests/requirements.txt + + - name: Download test model + id: download_model + run: | + ./scripts/hf.sh --repo TheBloke/Tinyllama-2-1b-miniguanaco-GGUF --file tinyllama-2-1b-miniguanaco.Q2_K.gguf + + - name: Server Integration Tests + id: server_integration_test + run: | + ./build/bin/server \ + -m tinyllama-2-1b-miniguanaco.Q2_K.gguf \ + --ctx-size 512 \ + --parallel 4 \ + --n-predict 512 \ + --batch-size 128 \ + --threads 4 \ + --threads-batch 128 \ + --alias phi-2 \ + --embedding \ + --cont-batching & + sh -c '\ + max_attempts=30; \ + attempts=${max_attempts}; \ + echo "waiting for server to be ready..."; \ + until curl --silent --show-error --fail "http://localhost:8080/health" | jq -r '.status' | grep ok; do \ + attempts=$(( attempts - 1)); \ + [ "${attempts}" -eq 0 ] && { echo "Server did not startup" >&2; exit 1; }; \ + sleep $(( (max_attempts - attempts) * 2 )); \ + done;' + cd examples/server/tests + behave + diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md new file mode 100644 index 0000000000000..3e0e2d8b1990a --- /dev/null +++ b/examples/server/tests/README.md @@ -0,0 +1,9 @@ +# Server Integration Test + +Functional server tests suite. + +### Install dependencies +`pip install -r requirements.txt` + +### Run tests +`python -m behave` diff --git a/examples/server/tests/features/completions.feature b/examples/server/tests/features/completions.feature new file mode 100644 index 0000000000000..4dc8786f68d12 --- /dev/null +++ b/examples/server/tests/features/completions.feature @@ -0,0 +1,11 @@ +Feature: Completion request + + Scenario Outline: run a completion request + Given a prompt + When we request a completion + Then tokens are predicted + + Examples: Prompts + | prompt | + | I believe the meaning of life is | + | Write a detailed analogy between mathematics and a lighthouse. | \ No newline at end of file diff --git a/examples/server/tests/features/oai.feature b/examples/server/tests/features/oai.feature new file mode 100644 index 0000000000000..d56aa8404a035 --- /dev/null +++ b/examples/server/tests/features/oai.feature @@ -0,0 +1,13 @@ +Feature: OpenAI compatible completions request + + Scenario Outline: run a completion on the OAI endpoint + Given a system prompt + And a user prompt + And a model + When we request the oai completions endpoint + Then the oai response contains completion tokens + + Examples: Prompts + | model | system_prompt | user_prompt | + | tinyllama-2 | You are ChatGPT. | I believe the meaning of life is | + | tinyllama-2 | You are a coding assistant. | Write the fibonacci function in c++ | \ No newline at end of file diff --git a/examples/server/tests/features/steps/completion.py b/examples/server/tests/features/steps/completion.py new file mode 100644 index 0000000000000..fda9a68e63fb3 --- /dev/null +++ b/examples/server/tests/features/steps/completion.py @@ -0,0 +1,24 @@ +from behave import * +import requests + + +@given(u'a prompt {prompt}') +def step_prompt(context, prompt): + context.prompt = prompt + + +@when(u'we request a completion') +def step_request_completion(context): + response = requests.post('http://localhost:8080/completion', json={ + "prompt": context.prompt + }) + status_code = response.status_code + assert status_code == 200 + context.response_data = response.json() + + +@then(u'tokens are predicted') +def step_request_completion(context): + assert len(context.response_data['content']) > 0 + assert context.response_data['timings']['predicted_n'] > 0 + diff --git a/examples/server/tests/features/steps/oai.py b/examples/server/tests/features/steps/oai.py new file mode 100644 index 0000000000000..0ed4ebd648d49 --- /dev/null +++ b/examples/server/tests/features/steps/oai.py @@ -0,0 +1,44 @@ +from behave import * +import openai + +openai.api_key = 'llama.cpp' +openai.api_base = "http://localhost:8080/v1/chat" + + +@given(u'a user prompt {user_prompt}') +def step_user_prompt(context, user_prompt): + context.user_prompt = user_prompt + + +@given(u'a system prompt {system_prompt}') +def step_system_prompt(context, system_prompt): + context.system_prompt = system_prompt + + +@given(u'a model {model}') +def step_model(context, model): + context.model = model + + +@when(u'we request the oai completions endpoint') +def step_oai_completions(context): + context.chat_completion = openai.Completion.create( + messages=[ + { + "role": "system", + "content": context.system_prompt, + }, + { + "role": "user", + "content": context.user_prompt, + } + ], + model=context.model, + ) + + +@then(u'the oai response contains completion tokens') +def step_oai_response_has_completion_tokens(context): + assert len(context.chat_completion.choices) == 1 + assert len(context.chat_completion.choices[0].message) > 0 + assert context.chat_completion.usage.completion_tokens > 0 diff --git a/examples/server/tests/requirements.txt b/examples/server/tests/requirements.txt new file mode 100644 index 0000000000000..f5c6f2e4aa355 --- /dev/null +++ b/examples/server/tests/requirements.txt @@ -0,0 +1,2 @@ +behave~=1.2.6 +openai~=0.25.0 \ No newline at end of file From 9b63d7057a5e0b3e6ad6fbb681d754182b3bc762 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Mon, 19 Feb 2024 21:50:56 +0100 Subject: [PATCH 02/98] server: tests: reduce number of files, all in one tests shell script --- .github/workflows/server-test.yml | 23 +--------- examples/server/tests/README.md | 4 +- .../server/tests/features/completions.feature | 11 ----- .../features/{oai.feature => server.feature} | 14 +++++- .../server/tests/features/steps/completion.py | 24 ---------- .../tests/features/steps/{oai.py => steps.py} | 24 +++++++++- examples/server/tests/tests.sh | 45 +++++++++++++++++++ 7 files changed, 85 insertions(+), 60 deletions(-) delete mode 100644 examples/server/tests/features/completions.feature rename examples/server/tests/features/{oai.feature => server.feature} (57%) delete mode 100644 examples/server/tests/features/steps/completion.py rename examples/server/tests/features/steps/{oai.py => steps.py} (66%) create mode 100755 examples/server/tests/tests.sh diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index d47c593f35c66..efd1bfcf39cb5 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -45,26 +45,7 @@ jobs: - name: Server Integration Tests id: server_integration_test run: | - ./build/bin/server \ - -m tinyllama-2-1b-miniguanaco.Q2_K.gguf \ - --ctx-size 512 \ - --parallel 4 \ - --n-predict 512 \ - --batch-size 128 \ - --threads 4 \ - --threads-batch 128 \ - --alias phi-2 \ - --embedding \ - --cont-batching & - sh -c '\ - max_attempts=30; \ - attempts=${max_attempts}; \ - echo "waiting for server to be ready..."; \ - until curl --silent --show-error --fail "http://localhost:8080/health" | jq -r '.status' | grep ok; do \ - attempts=$(( attempts - 1)); \ - [ "${attempts}" -eq 0 ] && { echo "Server did not startup" >&2; exit 1; }; \ - sleep $(( (max_attempts - attempts) * 2 )); \ - done;' cd examples/server/tests - behave + ./tests.sh + diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 3e0e2d8b1990a..975fee84831e8 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -6,4 +6,6 @@ Functional server tests suite. `pip install -r requirements.txt` ### Run tests -`python -m behave` +1. Build the server +2. download a GGUF model: `../../../scripts/hf.sh --repo TheBloke/Tinyllama-2-1b-miniguanaco-GGUF --file tinyllama-2-1b-miniguanaco.Q2_K.gguf` +3. Start the test: `./tests.sh tinyllama-2-1b-miniguanaco.Q2_K.gguf -ngl 23 --log-disable` diff --git a/examples/server/tests/features/completions.feature b/examples/server/tests/features/completions.feature deleted file mode 100644 index 4dc8786f68d12..0000000000000 --- a/examples/server/tests/features/completions.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: Completion request - - Scenario Outline: run a completion request - Given a prompt - When we request a completion - Then tokens are predicted - - Examples: Prompts - | prompt | - | I believe the meaning of life is | - | Write a detailed analogy between mathematics and a lighthouse. | \ No newline at end of file diff --git a/examples/server/tests/features/oai.feature b/examples/server/tests/features/server.feature similarity index 57% rename from examples/server/tests/features/oai.feature rename to examples/server/tests/features/server.feature index d56aa8404a035..60d8de9548cf4 100644 --- a/examples/server/tests/features/oai.feature +++ b/examples/server/tests/features/server.feature @@ -1,4 +1,14 @@ -Feature: OpenAI compatible completions request +Feature: llama.cpp server + + Scenario Outline: run a completion request + Given a prompt + When we request a completion + Then tokens are predicted + + Examples: Prompts + | prompt | + | I believe | + | Write a joke | Scenario Outline: run a completion on the OAI endpoint Given a system prompt @@ -9,5 +19,5 @@ Feature: OpenAI compatible completions request Examples: Prompts | model | system_prompt | user_prompt | - | tinyllama-2 | You are ChatGPT. | I believe the meaning of life is | + | tinyllama-2 | You are ChatGPT. | Say hello | | tinyllama-2 | You are a coding assistant. | Write the fibonacci function in c++ | \ No newline at end of file diff --git a/examples/server/tests/features/steps/completion.py b/examples/server/tests/features/steps/completion.py deleted file mode 100644 index fda9a68e63fb3..0000000000000 --- a/examples/server/tests/features/steps/completion.py +++ /dev/null @@ -1,24 +0,0 @@ -from behave import * -import requests - - -@given(u'a prompt {prompt}') -def step_prompt(context, prompt): - context.prompt = prompt - - -@when(u'we request a completion') -def step_request_completion(context): - response = requests.post('http://localhost:8080/completion', json={ - "prompt": context.prompt - }) - status_code = response.status_code - assert status_code == 200 - context.response_data = response.json() - - -@then(u'tokens are predicted') -def step_request_completion(context): - assert len(context.response_data['content']) > 0 - assert context.response_data['timings']['predicted_n'] > 0 - diff --git a/examples/server/tests/features/steps/oai.py b/examples/server/tests/features/steps/steps.py similarity index 66% rename from examples/server/tests/features/steps/oai.py rename to examples/server/tests/features/steps/steps.py index 0ed4ebd648d49..f2721097b9ddb 100644 --- a/examples/server/tests/features/steps/oai.py +++ b/examples/server/tests/features/steps/steps.py @@ -1,10 +1,32 @@ -from behave import * import openai +import requests +from behave import * openai.api_key = 'llama.cpp' openai.api_base = "http://localhost:8080/v1/chat" +@given(u'a prompt {prompt}') +def step_prompt(context, prompt): + context.prompt = prompt + + +@when(u'we request a completion') +def step_request_completion(context): + response = requests.post('http://localhost:8080/completion', json={ + "prompt": context.prompt + }) + status_code = response.status_code + assert status_code == 200 + context.response_data = response.json() + + +@then(u'tokens are predicted') +def step_request_completion(context): + assert len(context.response_data['content']) > 0 + assert context.response_data['timings']['predicted_n'] > 0 + + @given(u'a user prompt {user_prompt}') def step_user_prompt(context, user_prompt): context.user_prompt = user_prompt diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh new file mode 100755 index 0000000000000..d3d414cd3ae66 --- /dev/null +++ b/examples/server/tests/tests.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +if [ $# -lt 1 ] +then + >&2 echo "Usage: $0 model_path [server_args...]" + exit 1 +fi + +cleanup() { + pkill -P $$ +} +trap cleanup EXIT + +model_path="$1" +shift 1 + +set -eu + +# Start the server in background +../../../build/bin/server \ + --model "$model_path" \ + --alias tinyllama-2 \ + --ctx-size 64 \ + --parallel 2 \ + --n-predict 32 \ + --batch-size 32 \ + --threads 4 \ + --threads-batch 4 \ + --embedding \ + --cont-batching \ + "$@" & + +# Wait for the server to start +max_attempts=30 +attempts=${max_attempts} +until curl --silent --fail "http://localhost:8080/health" | jq -r '.status' | grep ok; do + attempts=$(( attempts - 1)); + [ "${attempts}" -eq 0 ] && { echo "Server did not startup" >&2; exit 1; } + sleep_time=$(( (max_attempts - attempts) * 2 )) + echo "waiting for server to be ready ${sleep_time}s..." + sleep ${sleep_time} +done + +# Start tests +behave \ No newline at end of file From 6497755de5c9f456162aa00a67d33321007c3974 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Mon, 19 Feb 2024 22:46:36 +0100 Subject: [PATCH 03/98] server: tests: fix ci workflow --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index efd1bfcf39cb5..74c9b9a2f389c 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -46,6 +46,6 @@ jobs: id: server_integration_test run: | cd examples/server/tests - ./tests.sh + ./tests.sh tinyllama-2-1b-miniguanaco.Q2_K.gguf From 4e5245e6b8c1dfec0a6b496f6764ae826f5da748 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Mon, 19 Feb 2024 22:52:56 +0100 Subject: [PATCH 04/98] server: tests: fix ci workflow --- examples/server/tests/tests.sh | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index d3d414cd3ae66..36297e9dbdd8f 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -6,6 +6,7 @@ then exit 1 fi +# kill the server at the end cleanup() { pkill -P $$ } @@ -30,16 +31,5 @@ set -eu --cont-batching \ "$@" & -# Wait for the server to start -max_attempts=30 -attempts=${max_attempts} -until curl --silent --fail "http://localhost:8080/health" | jq -r '.status' | grep ok; do - attempts=$(( attempts - 1)); - [ "${attempts}" -eq 0 ] && { echo "Server did not startup" >&2; exit 1; } - sleep_time=$(( (max_attempts - attempts) * 2 )) - echo "waiting for server to be ready ${sleep_time}s..." - sleep ${sleep_time} -done - # Start tests behave \ No newline at end of file From 30aa323fb906d39dd5ea96b62beabf6f77924e9f Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Mon, 19 Feb 2024 23:01:13 +0100 Subject: [PATCH 05/98] server: tests: fix ci workflow --- .github/workflows/server-test.yml | 2 +- examples/server/tests/tests.sh | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 74c9b9a2f389c..c39e5dd1dd435 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -46,6 +46,6 @@ jobs: id: server_integration_test run: | cd examples/server/tests - ./tests.sh tinyllama-2-1b-miniguanaco.Q2_K.gguf + ./tests.sh ../../../tinyllama-2-1b-miniguanaco.Q2_K.gguf diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index 36297e9dbdd8f..d3d414cd3ae66 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -6,7 +6,6 @@ then exit 1 fi -# kill the server at the end cleanup() { pkill -P $$ } @@ -31,5 +30,16 @@ set -eu --cont-batching \ "$@" & +# Wait for the server to start +max_attempts=30 +attempts=${max_attempts} +until curl --silent --fail "http://localhost:8080/health" | jq -r '.status' | grep ok; do + attempts=$(( attempts - 1)); + [ "${attempts}" -eq 0 ] && { echo "Server did not startup" >&2; exit 1; } + sleep_time=$(( (max_attempts - attempts) * 2 )) + echo "waiting for server to be ready ${sleep_time}s..." + sleep ${sleep_time} +done + # Start tests behave \ No newline at end of file From fe9866a52de720632e0125e6dd1d65caae0471c8 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Mon, 19 Feb 2024 23:05:06 +0100 Subject: [PATCH 06/98] server: tests: use ngxson llama_xs_q4.bin --- .github/workflows/server-test.yml | 4 ++-- examples/server/tests/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index c39e5dd1dd435..9e5a5cd8d96f4 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -40,12 +40,12 @@ jobs: - name: Download test model id: download_model run: | - ./scripts/hf.sh --repo TheBloke/Tinyllama-2-1b-miniguanaco-GGUF --file tinyllama-2-1b-miniguanaco.Q2_K.gguf + ./scripts/hf.sh --repo ngxson/dummy-llama --file llama_xs_q4.bin - name: Server Integration Tests id: server_integration_test run: | cd examples/server/tests - ./tests.sh ../../../tinyllama-2-1b-miniguanaco.Q2_K.gguf + ./tests.sh ../../../llama_xs_q4.bin diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 975fee84831e8..ae8ae74f3488f 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -7,5 +7,5 @@ Functional server tests suite. ### Run tests 1. Build the server -2. download a GGUF model: `../../../scripts/hf.sh --repo TheBloke/Tinyllama-2-1b-miniguanaco-GGUF --file tinyllama-2-1b-miniguanaco.Q2_K.gguf` +2. download a GGUF model: `../../../scripts/hf.sh --repo ngxson/dummy-llama --file llama_xs_q4.bin` 3. Start the test: `./tests.sh tinyllama-2-1b-miniguanaco.Q2_K.gguf -ngl 23 --log-disable` From 1680599b01a7fd32ecf07640e1da430d393f7874 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Mon, 19 Feb 2024 23:10:39 +0100 Subject: [PATCH 07/98] server: tests: build only the server --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 9e5a5cd8d96f4..c2bb268190a29 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -30,7 +30,7 @@ jobs: mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=Release - cmake --build . --config Release -j $(nproc) + cmake --build . --config Release -j $(nproc) --target server - name: Tests dependencies id: test_dependencies From 8bb586bf066f1230e81a12cd9dcc812b9c4d75a4 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 01:15:31 +0100 Subject: [PATCH 08/98] server: tests: add health check and concurrent request example --- examples/server/tests/features/server.feature | 22 ++- examples/server/tests/features/steps/steps.py | 142 ++++++++++++++++-- examples/server/tests/tests.sh | 16 +- 3 files changed, 151 insertions(+), 29 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 60d8de9548cf4..6ab35b2e7f2be 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -1,5 +1,13 @@ Feature: llama.cpp server + Background: The server is started and ready to accept prompts + When wait for the server to be started + Then wait for the server to be healthy + + Scenario: Health endpoint + Given an health liveness probe + Then the server must be healthy + Scenario Outline: run a completion request Given a prompt When we request a completion @@ -18,6 +26,14 @@ Feature: llama.cpp server Then the oai response contains completion tokens Examples: Prompts - | model | system_prompt | user_prompt | - | tinyllama-2 | You are ChatGPT. | Say hello | - | tinyllama-2 | You are a coding assistant. | Write the fibonacci function in c++ | \ No newline at end of file + | model | system_prompt | user_prompt | + | tinyllama-2 | You are ChatGPT. | Say hello | + | tinyllama-2 | You are a coding assistant. | Write the fibonacci function in c++ | + + + Scenario: Health endpoint during processing with concurrent requests + Given 2 slow concurrent prompts + Then wait for all slots processing + Then the server is overloaded + When wait for all slots idle + Then all prompts must be predicted \ No newline at end of file diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index f2721097b9ddb..a9933a724d9ba 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -1,19 +1,77 @@ +import socket +import threading +import time +from contextlib import closing + import openai import requests -from behave import * +from behave import step +from behave.api.async_step import async_run_until_complete + +base_fqdn = 'localhost' +base_port = 8080 +base_url = f"http://{base_fqdn}:{base_port}" openai.api_key = 'llama.cpp' -openai.api_base = "http://localhost:8080/v1/chat" +openai.api_base = f"{base_url}/v1/chat" + +slow_prompt = 'say hello ' * 10 +fast_prompt = 'Write a joke' + +n_slots = 2 + + +@step(u'wait for the server to be started') +def step_wait_for_the_server_to_be_started(context): + server_started = False + while not server_started: + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + result = sock.connect_ex((base_fqdn, base_port)) + if result != 0: + print("server not ready: ", base_fqdn, base_port, result) + time.sleep(1) + else: + return 0 + + +@step(u'wait for the server to be healthy') +def step_wait_for_the_server_to_be_healthy(context): + status_code = 500 + while status_code != 200: + status_code = requests.get(f'{base_url}/health').status_code + if status_code != 200: + time.sleep(1) -@given(u'a prompt {prompt}') +@step(u'an health liveness probe') +def step_an_health_liveness_probe(context): + response = requests.get(f'{base_url}/health') + context.status_code = response.status_code + context.response_data = response.json() + + +@step(u'the server must be healthy') +def step_server_healthy(context): + assert context.status_code == 200 + assert context.response_data['status'] == 'ok' + + +@step(u'the server is overloaded') +@async_run_until_complete() +async def step_server_overloaded(context): + response = requests.get(f'{base_url}/health?fail_on_no_slot') + assert response.status_code == 503 + assert response.json()['status'] == 'no slot available' + + +@step(u'a prompt {prompt}') def step_prompt(context, prompt): context.prompt = prompt -@when(u'we request a completion') +@step(u'we request a completion') def step_request_completion(context): - response = requests.post('http://localhost:8080/completion', json={ + response = requests.post(f'{base_url}/completion', json={ "prompt": context.prompt }) status_code = response.status_code @@ -21,28 +79,27 @@ def step_request_completion(context): context.response_data = response.json() -@then(u'tokens are predicted') +@step(u'tokens are predicted') def step_request_completion(context): - assert len(context.response_data['content']) > 0 - assert context.response_data['timings']['predicted_n'] > 0 + prompt_predicted(context.response_data) -@given(u'a user prompt {user_prompt}') +@step(u'a user prompt {user_prompt}') def step_user_prompt(context, user_prompt): context.user_prompt = user_prompt -@given(u'a system prompt {system_prompt}') +@step(u'a system prompt {system_prompt}') def step_system_prompt(context, system_prompt): context.system_prompt = system_prompt -@given(u'a model {model}') +@step(u'a model {model}') def step_model(context, model): context.model = model -@when(u'we request the oai completions endpoint') +@step(u'we request the oai completions endpoint') def step_oai_completions(context): context.chat_completion = openai.Completion.create( messages=[ @@ -59,8 +116,67 @@ def step_oai_completions(context): ) -@then(u'the oai response contains completion tokens') +@step(u'the oai response contains completion tokens') def step_oai_response_has_completion_tokens(context): assert len(context.chat_completion.choices) == 1 assert len(context.chat_completion.choices[0].message) > 0 assert context.chat_completion.usage.completion_tokens > 0 + + +def async_prompt(context, prompt): + response = requests.post(f'{base_url}/completion', json={ + "prompt": prompt + }) + + context.async_responses.append(response) + + +@step(u'{n_prompt} {prompt_type} concurrent prompts') +def step_n_concurrent_prompts(context, n_prompt, prompt_type): + prompt = fast_prompt + if prompt_type == 'slow': + prompt = slow_prompt + context.async_responses = [] + context.threads = [] + for i in range(int(n_prompt)): + thread = threading.Thread(target=async_prompt, args=(context, prompt)) + thread.start() + context.threads.append(thread) + + +def wait_for_slots_processing(context, expected_slots_processing): + while True: + health = requests.get(f'{base_url}/health').json() + if 'slots_processing' in health: # FIXME when #5594 is merged + slots_processing = health['slots_processing'] + else: + slots_processing = 0 + if slots_processing == expected_slots_processing: + break + else: + time.sleep(0.2) + + +@step(u'wait for all slots processing') +def step_wait_for_all_slots_processing(context): + wait_for_slots_processing(context, n_slots) + + +@step(u'wait for all slots idle') +def step_wait_for_all_slots_idle(context): + wait_for_slots_processing(context, 0) + + +@step(u'all prompts must be predicted') +def step_all_prompts_must_be_predicted(context): + for thread in context.threads: + thread.join() + for async_response in context.async_responses: + assert async_response.status_code == 200 + response_data = async_response.json() + prompt_predicted(response_data) + + +def prompt_predicted(response_data): + assert len(response_data['content']) > 0 + assert response_data['timings']['predicted_n'] > 0 diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index d3d414cd3ae66..01b2f5d4d5155 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -6,6 +6,7 @@ then exit 1 fi +# kill the server at the end cleanup() { pkill -P $$ } @@ -20,9 +21,9 @@ set -eu ../../../build/bin/server \ --model "$model_path" \ --alias tinyllama-2 \ - --ctx-size 64 \ + --ctx-size 1024 \ --parallel 2 \ - --n-predict 32 \ + --n-predict 1024 \ --batch-size 32 \ --threads 4 \ --threads-batch 4 \ @@ -30,16 +31,5 @@ set -eu --cont-batching \ "$@" & -# Wait for the server to start -max_attempts=30 -attempts=${max_attempts} -until curl --silent --fail "http://localhost:8080/health" | jq -r '.status' | grep ok; do - attempts=$(( attempts - 1)); - [ "${attempts}" -eq 0 ] && { echo "Server did not startup" >&2; exit 1; } - sleep_time=$(( (max_attempts - attempts) * 2 )) - echo "waiting for server to be ready ${sleep_time}s..." - sleep ${sleep_time} -done - # Start tests behave \ No newline at end of file From 6c95ec65876818115a0bb790ad8ed4a8e0a6ac4e Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 20:50:14 +0100 Subject: [PATCH 09/98] server: tests: change model to: @karpathy's tinyllamas --- .github/workflows/server-test.yml | 4 ++-- examples/server/tests/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index c2bb268190a29..d05230fbd5d1c 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -40,12 +40,12 @@ jobs: - name: Download test model id: download_model run: | - ./scripts/hf.sh --repo ngxson/dummy-llama --file llama_xs_q4.bin + ./scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf - name: Server Integration Tests id: server_integration_test run: | cd examples/server/tests - ./tests.sh ../../../llama_xs_q4.bin + ./tests.sh ../../../stories260K.gguf diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index ae8ae74f3488f..3cdcc5ca3e151 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -7,5 +7,5 @@ Functional server tests suite. ### Run tests 1. Build the server -2. download a GGUF model: `../../../scripts/hf.sh --repo ngxson/dummy-llama --file llama_xs_q4.bin` -3. Start the test: `./tests.sh tinyllama-2-1b-miniguanaco.Q2_K.gguf -ngl 23 --log-disable` +2. download a GGUF model: `./scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf` +3. Start the test: `./tests.sh stories260K.gguf -ngl 23` From 56583bee41950f80f55953b73433c4afc96d82f2 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 20:52:24 +0100 Subject: [PATCH 10/98] server: tests: refactor steps and vocabulary --- examples/server/tests/features/server.feature | 77 ++++--- examples/server/tests/features/steps/steps.py | 212 ++++++++---------- 2 files changed, 138 insertions(+), 151 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 6ab35b2e7f2be..d2e691f12c828 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -1,39 +1,58 @@ Feature: llama.cpp server - Background: The server is started and ready to accept prompts - When wait for the server to be started - Then wait for the server to be healthy + Background: Server startup + Given a server listening on localhost:8080 with 2 slots + Then the server is starting + Then the server is healthy - Scenario: Health endpoint - Given an health liveness probe - Then the server must be healthy + Scenario: Health + When the server is healthy + Then the server is ready - Scenario Outline: run a completion request - Given a prompt - When we request a completion - Then tokens are predicted + Scenario Outline: Completion + Given a completion request with maximum tokens + Then tokens are predicted Examples: Prompts - | prompt | - | I believe | - | Write a joke | + | prompt | n_predict | predicted_n | + | I believe the meaning of life is | 128 | 128 | + | Write a joke about AI | 512 | 512 | - Scenario Outline: run a completion on the OAI endpoint + Scenario Outline: OAI Compatibility Given a system prompt - And a user prompt - And a model - When we request the oai completions endpoint - Then the oai response contains completion tokens + And a user prompt + And a model + And max tokens to predict + Given an OAI compatible chat completions request + Then tokens are predicted Examples: Prompts - | model | system_prompt | user_prompt | - | tinyllama-2 | You are ChatGPT. | Say hello | - | tinyllama-2 | You are a coding assistant. | Write the fibonacci function in c++ | - - - Scenario: Health endpoint during processing with concurrent requests - Given 2 slow concurrent prompts - Then wait for all slots processing - Then the server is overloaded - When wait for all slots idle - Then all prompts must be predicted \ No newline at end of file + | model | system_prompt | user_prompt | max_tokens | predicted_n | + | llama-2 | You are ChatGPT. | Say hello. | 64 | 64 | + | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | 512 | + + Scenario: Multi users + Given a prompt: + """ + Write a formal complaint email to Air France about my delayed + baggage from my flight on Tuesday, January 17th, from Paris to Toulouse. Be verbose. + """ + And a prompt: + """ + Translate the following War & Peace chapter into Russian: WELL, PRINCE, + Genoa and Lucca are now no more than private estates of the Bonaparte + family. No, I warn you, that if you do not tell me we are at war, + if you again allow yourself to palliate all the infamies and atrocities + of this Antichrist (upon my word, I believe he is), I don’t know you + in future, you are no longer my friend, no longer my faithful slave, + as you say. There, how do you do, how do you do? I see I’m scaring you, + sit down and talk to me.” These words were uttered in July 1805 by + Anna Pavlovna Scherer, a distinguished lady of the court, + and confidential maid-of-honour to the Empress Marya Fyodorovna. + It was her greeting to Prince Vassily, a man high in rank + and office, who was the first to arrive at her soirée. + """ + Given concurrent completion requests + Then the server is busy + Then the server is idle + Then all prompts are predicted \ No newline at end of file diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index a9933a724d9ba..c6fbff84d06e3 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -6,82 +6,52 @@ import openai import requests from behave import step -from behave.api.async_step import async_run_until_complete -base_fqdn = 'localhost' -base_port = 8080 -base_url = f"http://{base_fqdn}:{base_port}" -openai.api_key = 'llama.cpp' -openai.api_base = f"{base_url}/v1/chat" +@step(u"a server listening on {server_fqdn}:{server_port} with {n_slots} slots") +def step_server_config(context, server_fqdn, server_port, n_slots): + context.server_fqdn = server_fqdn + context.server_port = int(server_port) + context.n_slots = int(n_slots) + context.base_url = f'http://{context.server_fqdn}:{context.server_port}' -slow_prompt = 'say hello ' * 10 -fast_prompt = 'Write a joke' + context.completions = [] + context.prompts = [] -n_slots = 2 + openai.api_key = 'llama.cpp' + openai.api_base = f'{context.base_url}/v1/chat' -@step(u'wait for the server to be started') -def step_wait_for_the_server_to_be_started(context): - server_started = False - while not server_started: - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: - result = sock.connect_ex((base_fqdn, base_port)) - if result != 0: - print("server not ready: ", base_fqdn, base_port, result) - time.sleep(1) - else: - return 0 +@step(u"the server is {expecting_status}") +def step_wait_for_the_server_to_be_started(context, expecting_status): + match expecting_status: + case 'starting': + server_started = False + while not server_started: + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + result = sock.connect_ex((context.server_fqdn, context.server_port)) + if result == 0: + return 0 + case 'loading model': + wait_for_health_status(context, 503, 'loading model') + case 'healthy': + wait_for_health_status(context, 200, 'ok') + case 'ready' | 'idle': + wait_for_health_status(context, 200, 'ok', params={'fail_on_no_slot': True}) + case 'busy': + wait_for_health_status(context, 503, 'no slot available', params={'fail_on_no_slot': True}) + case _: + assert False, "unknown status" -@step(u'wait for the server to be healthy') -def step_wait_for_the_server_to_be_healthy(context): - status_code = 500 - while status_code != 200: - status_code = requests.get(f'{base_url}/health').status_code - if status_code != 200: - time.sleep(1) - - -@step(u'an health liveness probe') -def step_an_health_liveness_probe(context): - response = requests.get(f'{base_url}/health') - context.status_code = response.status_code - context.response_data = response.json() - - -@step(u'the server must be healthy') -def step_server_healthy(context): - assert context.status_code == 200 - assert context.response_data['status'] == 'ok' - - -@step(u'the server is overloaded') -@async_run_until_complete() -async def step_server_overloaded(context): - response = requests.get(f'{base_url}/health?fail_on_no_slot') - assert response.status_code == 503 - assert response.json()['status'] == 'no slot available' - - -@step(u'a prompt {prompt}') -def step_prompt(context, prompt): - context.prompt = prompt - - -@step(u'we request a completion') -def step_request_completion(context): - response = requests.post(f'{base_url}/completion', json={ - "prompt": context.prompt - }) - status_code = response.status_code - assert status_code == 200 - context.response_data = response.json() +@step(u'a {prompt} completion request with maximum {n_predict} tokens') +def step_request_completion(context, prompt, n_predict): + request_completion(context, prompt, n_predict) -@step(u'tokens are predicted') -def step_request_completion(context): - prompt_predicted(context.response_data) +@step(u'{predicted_n} tokens are predicted') +def step_n_tokens_predicted(context, predicted_n): + assert_n_tokens_predicted(context.completions[0], int(predicted_n)) @step(u'a user prompt {user_prompt}') @@ -99,9 +69,14 @@ def step_model(context, model): context.model = model -@step(u'we request the oai completions endpoint') -def step_oai_completions(context): - context.chat_completion = openai.Completion.create( +@step(u'{max_tokens} max tokens to predict') +def step_max_tokens(context, max_tokens): + context.max_tokens = int(max_tokens) + + +@step(u'an OAI compatible chat completions request') +def step_oai_chat_completions(context): + chat_completion = openai.Completion.create( messages=[ { "role": "system", @@ -113,70 +88,63 @@ def step_oai_completions(context): } ], model=context.model, + max_tokens=context.max_tokens ) - - -@step(u'the oai response contains completion tokens') -def step_oai_response_has_completion_tokens(context): - assert len(context.chat_completion.choices) == 1 - assert len(context.chat_completion.choices[0].message) > 0 - assert context.chat_completion.usage.completion_tokens > 0 - - -def async_prompt(context, prompt): - response = requests.post(f'{base_url}/completion', json={ - "prompt": prompt + context.completions.append({ + 'content': chat_completion.choices[0].message, + 'timings': { + 'predicted_n': chat_completion.usage.completion_tokens + } }) - context.async_responses.append(response) - -@step(u'{n_prompt} {prompt_type} concurrent prompts') -def step_n_concurrent_prompts(context, n_prompt, prompt_type): - prompt = fast_prompt - if prompt_type == 'slow': - prompt = slow_prompt - context.async_responses = [] - context.threads = [] - for i in range(int(n_prompt)): - thread = threading.Thread(target=async_prompt, args=(context, prompt)) - thread.start() - context.threads.append(thread) +@step(u'a prompt') +def step_a_prompt(context): + context.prompts.append(context.text) -def wait_for_slots_processing(context, expected_slots_processing): - while True: - health = requests.get(f'{base_url}/health').json() - if 'slots_processing' in health: # FIXME when #5594 is merged - slots_processing = health['slots_processing'] - else: - slots_processing = 0 - if slots_processing == expected_slots_processing: - break - else: - time.sleep(0.2) +@step(u'concurrent completion requests') +def step_n_concurrent_prompts(context): + context.completions.clear() + context.completion_threads = [] + for prompt in context.prompts: + completion_thread = threading.Thread(target=request_completion, args=(context, prompt)) + completion_thread.start() + context.completion_threads.append(completion_thread) -@step(u'wait for all slots processing') -def step_wait_for_all_slots_processing(context): - wait_for_slots_processing(context, n_slots) +@step(u'all prompts are predicted') +def step_all_prompts_must_be_predicted(context): + for completion_thread in context.completion_threads: + completion_thread.join() + for completion in context.completions: + assert_n_tokens_predicted(completion) -@step(u'wait for all slots idle') -def step_wait_for_all_slots_idle(context): - wait_for_slots_processing(context, 0) +def request_completion(context, prompt, n_predict=None): + response = requests.post(f'{context.base_url}/completion', json={ + "prompt": prompt, + "n_predict": int(n_predict) if n_predict is not None else 4096, + }) + status_code = response.status_code + assert status_code == 200 + context.completions.append(response.json()) -@step(u'all prompts must be predicted') -def step_all_prompts_must_be_predicted(context): - for thread in context.threads: - thread.join() - for async_response in context.async_responses: - assert async_response.status_code == 200 - response_data = async_response.json() - prompt_predicted(response_data) +def assert_n_tokens_predicted(completion_response, expected_predicted_n=None): + content = completion_response['content'] + n_predicted = completion_response['timings']['predicted_n'] + assert len(content) > 0, "no token predicted" + if expected_predicted_n is not None: + assert n_predicted == expected_predicted_n, (f'invalid number of tokens predicted:' + f' "{n_predicted}" <> "{expected_predicted_n}"') -def prompt_predicted(response_data): - assert len(response_data['content']) > 0 - assert response_data['timings']['predicted_n'] > 0 +def wait_for_health_status(context, expected_http_status_code, expected_health_status, params=None): + status_code = 500 + while status_code != expected_http_status_code: + health_response = requests.get(f'{context.base_url}/health', params) + status_code = health_response.status_code + health = health_response.json() + if status_code != expected_http_status_code or health['status'] != expected_health_status: + time.sleep(0.001) From 9b7ea97979a087a8ffbcba5368fa81385d6580bf Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 21:34:35 +0100 Subject: [PATCH 11/98] server: tests: add OAI stream test, fix file end of line, fast fail behave --- .github/workflows/server-test.yml | 2 -- examples/server/tests/features/server.feature | 9 ++--- examples/server/tests/features/steps/steps.py | 34 +++++++++++++++---- examples/server/tests/requirements.txt | 2 +- examples/server/tests/tests.sh | 2 +- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index d05230fbd5d1c..b70006e04a4be 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -47,5 +47,3 @@ jobs: run: | cd examples/server/tests ./tests.sh ../../../stories260K.gguf - - diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index d2e691f12c828..a14d1459a8afd 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -23,13 +23,14 @@ Feature: llama.cpp server And a user prompt And a model And max tokens to predict + And streaming is Given an OAI compatible chat completions request Then tokens are predicted Examples: Prompts - | model | system_prompt | user_prompt | max_tokens | predicted_n | - | llama-2 | You are ChatGPT. | Say hello. | 64 | 64 | - | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | 512 | + | model | system_prompt | user_prompt | max_tokens | enable_streaming | predicted_n | + | llama-2 | You are ChatGPT. | Say hello. | 64 | false | 64 | + | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | true | 512 | Scenario: Multi users Given a prompt: @@ -55,4 +56,4 @@ Feature: llama.cpp server Given concurrent completion requests Then the server is busy Then the server is idle - Then all prompts are predicted \ No newline at end of file + Then all prompts are predicted diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index c6fbff84d06e3..f9823b51f2e4e 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -74,6 +74,11 @@ def step_max_tokens(context, max_tokens): context.max_tokens = int(max_tokens) +@step(u'streaming is {enable_streaming}') +def step_streaming(context, enable_streaming): + context.enable_streaming = bool(enable_streaming) + + @step(u'an OAI compatible chat completions request') def step_oai_chat_completions(context): chat_completion = openai.Completion.create( @@ -88,14 +93,31 @@ def step_oai_chat_completions(context): } ], model=context.model, - max_tokens=context.max_tokens + max_tokens=context.max_tokens, + stream=context.enable_streaming ) - context.completions.append({ - 'content': chat_completion.choices[0].message, - 'timings': { - 'predicted_n': chat_completion.usage.completion_tokens + if context.enable_streaming: + completion_response = { + 'content': '', + 'timings': { + 'predicted_n': 0 + } } - }) + for chunk in chat_completion: + assert len(chunk.choices) == 1 + delta = chunk.choices[0].delta + if 'content' in delta: + completion_response['content'] += delta['content'] + completion_response['timings']['predicted_n'] += 1 + context.completions.append(completion_response) + else: + assert len(chat_completion.choices) == 1 + context.completions.append({ + 'content': chat_completion.choices[0].message, + 'timings': { + 'predicted_n': chat_completion.usage.completion_tokens + } + }) @step(u'a prompt') diff --git a/examples/server/tests/requirements.txt b/examples/server/tests/requirements.txt index f5c6f2e4aa355..b64fbc6ba0c21 100644 --- a/examples/server/tests/requirements.txt +++ b/examples/server/tests/requirements.txt @@ -1,2 +1,2 @@ behave~=1.2.6 -openai~=0.25.0 \ No newline at end of file +openai~=0.25.0 diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index 01b2f5d4d5155..230ee45add9be 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -32,4 +32,4 @@ set -eu "$@" & # Start tests -behave \ No newline at end of file +behave --summary --stop From 11adf1d8644c5343beec221b84613d76f78d517b Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 22:00:09 +0100 Subject: [PATCH 12/98] server: tests: add OAI multi user scenario --- examples/server/tests/features/server.feature | 23 +++++ examples/server/tests/features/steps/steps.py | 92 +++++++++++-------- 2 files changed, 76 insertions(+), 39 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index a14d1459a8afd..78ba2bec96ad8 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -57,3 +57,26 @@ Feature: llama.cpp server Then the server is busy Then the server is idle Then all prompts are predicted + + + Scenario: Multi users OAI Compatibility + Given a system prompt "You are an AI assistant." + And a model tinyllama-2 + And 1024 max tokens to predict + And streaming is enabled + Given a prompt: + """ + Write a very long story about AI. + """ + And a prompt: + """ + Write another very long music lyrics. + """ + And a prompt: + """ + Write yet another very long music lyrics. + """ + Given concurrent OAI completions requests + Then the server is busy + Then the server is idle + Then all prompts are predicted \ No newline at end of file diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index f9823b51f2e4e..6d714ae92c80a 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -16,6 +16,7 @@ def step_server_config(context, server_fqdn, server_port, n_slots): context.base_url = f'http://{context.server_fqdn}:{context.server_port}' context.completions = [] + context.completion_threads = [] context.prompts = [] openai.api_key = 'llama.cpp' @@ -76,11 +77,58 @@ def step_max_tokens(context, max_tokens): @step(u'streaming is {enable_streaming}') def step_streaming(context, enable_streaming): - context.enable_streaming = bool(enable_streaming) + context.enable_streaming = enable_streaming == 'enabled' or bool(enable_streaming) @step(u'an OAI compatible chat completions request') def step_oai_chat_completions(context): + oai_chat_completions(context, context.user_prompt) + + +@step(u'a prompt') +def step_a_prompt(context): + context.prompts.append(context.text) + + +@step(u'concurrent completion requests') +def step_concurrent_completion_requests(context): + concurrent_requests(context, request_completion) + + +@step(u'concurrent OAI completions requests') +def step_oai_chat_completions(context): + concurrent_requests(context, oai_chat_completions) + + +@step(u'all prompts are predicted') +def step_all_prompts_are_predicted(context): + for completion_thread in context.completion_threads: + completion_thread.join() + for completion in context.completions: + assert_n_tokens_predicted(completion) + + +def concurrent_requests(context, f_completion): + context.completions.clear() + context.completion_threads.clear() + for prompt in context.prompts: + completion_thread = threading.Thread(target=f_completion, args=(context, prompt)) + completion_thread.start() + context.completion_threads.append(completion_thread) + context.prompts.clear() + + +def request_completion(context, prompt, n_predict=None): + response = requests.post(f'{context.base_url}/completion', json={ + "prompt": prompt, + "n_predict": int(n_predict) if n_predict is not None else 4096, + }) + status_code = response.status_code + assert status_code == 200 + context.completions.append(response.json()) + + +def oai_chat_completions(context, user_prompt): chat_completion = openai.Completion.create( messages=[ { @@ -89,7 +137,7 @@ def step_oai_chat_completions(context): }, { "role": "user", - "content": context.user_prompt, + "content": user_prompt, } ], model=context.model, @@ -120,39 +168,6 @@ def step_oai_chat_completions(context): }) -@step(u'a prompt') -def step_a_prompt(context): - context.prompts.append(context.text) - - -@step(u'concurrent completion requests') -def step_n_concurrent_prompts(context): - context.completions.clear() - context.completion_threads = [] - for prompt in context.prompts: - completion_thread = threading.Thread(target=request_completion, args=(context, prompt)) - completion_thread.start() - context.completion_threads.append(completion_thread) - - -@step(u'all prompts are predicted') -def step_all_prompts_must_be_predicted(context): - for completion_thread in context.completion_threads: - completion_thread.join() - for completion in context.completions: - assert_n_tokens_predicted(completion) - - -def request_completion(context, prompt, n_predict=None): - response = requests.post(f'{context.base_url}/completion', json={ - "prompt": prompt, - "n_predict": int(n_predict) if n_predict is not None else 4096, - }) - status_code = response.status_code - assert status_code == 200 - context.completions.append(response.json()) - - def assert_n_tokens_predicted(completion_response, expected_predicted_n=None): content = completion_response['content'] n_predicted = completion_response['timings']['predicted_n'] @@ -163,10 +178,9 @@ def assert_n_tokens_predicted(completion_response, expected_predicted_n=None): def wait_for_health_status(context, expected_http_status_code, expected_health_status, params=None): - status_code = 500 - while status_code != expected_http_status_code: + while True: health_response = requests.get(f'{context.base_url}/health', params) status_code = health_response.status_code health = health_response.json() - if status_code != expected_http_status_code or health['status'] != expected_health_status: - time.sleep(0.001) + if status_code == expected_http_status_code and health['status'] == expected_health_status: + break From c355f76427f5b245b76b7dd86cd797055a0f5194 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 22:32:11 +0100 Subject: [PATCH 13/98] server: tests: slots endpoint checks --- examples/server/tests/features/server.feature | 5 ++ examples/server/tests/features/steps/steps.py | 62 ++++++++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 78ba2bec96ad8..5a580e5f8771a 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -8,6 +8,7 @@ Feature: llama.cpp server Scenario: Health When the server is healthy Then the server is ready + And all slots are idle Scenario Outline: Completion Given a completion request with maximum tokens @@ -55,7 +56,9 @@ Feature: llama.cpp server """ Given concurrent completion requests Then the server is busy + And all slots are busy Then the server is idle + And all slots are idle Then all prompts are predicted @@ -78,5 +81,7 @@ Feature: llama.cpp server """ Given concurrent OAI completions requests Then the server is busy + And all slots are busy Then the server is idle + And all slots are idle Then all prompts are predicted \ No newline at end of file diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 6d714ae92c80a..72857c45e145d 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -1,6 +1,5 @@ import socket import threading -import time from contextlib import closing import openai @@ -38,13 +37,46 @@ def step_wait_for_the_server_to_be_started(context, expecting_status): case 'healthy': wait_for_health_status(context, 200, 'ok') case 'ready' | 'idle': - wait_for_health_status(context, 200, 'ok', params={'fail_on_no_slot': True}) + wait_for_health_status(context, 200, 'ok', + params={'fail_on_no_slot': True}, + slots_idle=context.n_slots, + slots_processing=0) + request_slots_status(context, [ + {'id': 0, 'state': 0}, + {'id': 1, 'state': 0} + ]) case 'busy': - wait_for_health_status(context, 503, 'no slot available', params={'fail_on_no_slot': True}) + wait_for_health_status(context, 503, 'no slot available', + params={'fail_on_no_slot': True}, + slots_idle=0, + slots_processing=context.n_slots) + request_slots_status(context, [ + {'id': 0, 'state': 1}, + {'id': 1, 'state': 1} + ]) case _: assert False, "unknown status" +@step(u'all slots are {expected_slot_status_string}') +def step_all_slots_status(context, expected_slot_status_string): + match expected_slot_status_string: + case 'idle': + expected_slot_status = 0 + case 'busy': + expected_slot_status = 1 + case _: + assert False, "unknown status" + + expected_slots = [] + for slot_id in range(context.n_slots): + expected_slots.append({ + 'id': slot_id, + 'state': expected_slot_status + }) + request_slots_status(context, expected_slots) + + @step(u'a {prompt} completion request with maximum {n_predict} tokens') def step_request_completion(context, prompt, n_predict): request_completion(context, prompt, n_predict) @@ -123,8 +155,7 @@ def request_completion(context, prompt, n_predict=None): "prompt": prompt, "n_predict": int(n_predict) if n_predict is not None else 4096, }) - status_code = response.status_code - assert status_code == 200 + assert response.status_code == 200 context.completions.append(response.json()) @@ -177,10 +208,27 @@ def assert_n_tokens_predicted(completion_response, expected_predicted_n=None): f' "{n_predicted}" <> "{expected_predicted_n}"') -def wait_for_health_status(context, expected_http_status_code, expected_health_status, params=None): +def wait_for_health_status(context, expected_http_status_code, + expected_health_status, + params=None, + slots_idle=None, + slots_processing=None): while True: health_response = requests.get(f'{context.base_url}/health', params) status_code = health_response.status_code health = health_response.json() - if status_code == expected_http_status_code and health['status'] == expected_health_status: + if (status_code == expected_http_status_code + and health['status'] == expected_health_status + and (slots_idle is None or health['slots_idle'] == slots_idle) + and (slots_processing is None or health['slots_processing'] == slots_processing)): break + + +def request_slots_status(context, expected_slots): + slots_response = requests.get(f'{context.base_url}/slots') + assert slots_response.status_code == 200 + slots = slots_response.json() + assert len(slots) == len(expected_slots) + for expected, slot in zip(expected_slots, slots): + for key in expected: + assert expected[key] == slot[key], f"expected[{key}] != slot[{key}]" From 367b59a15cab84e6f25c2e65a26833f7019511c7 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 22:45:30 +0100 Subject: [PATCH 14/98] server: tests: check for infinite loops --- examples/server/tests/features/server.feature | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 5a580e5f8771a..c4d821d74dbfd 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -54,6 +54,10 @@ Feature: llama.cpp server It was her greeting to Prince Vassily, a man high in rank and office, who was the first to arrive at her soirée. """ + And a prompt: + """ + Write another very long music lyrics. + """ Given concurrent completion requests Then the server is busy And all slots are busy @@ -65,7 +69,7 @@ Feature: llama.cpp server Scenario: Multi users OAI Compatibility Given a system prompt "You are an AI assistant." And a model tinyllama-2 - And 1024 max tokens to predict + And 512 max tokens to predict And streaming is enabled Given a prompt: """ @@ -77,11 +81,12 @@ Feature: llama.cpp server """ And a prompt: """ - Write yet another very long music lyrics. + I believe the meaning of life is """ Given concurrent OAI completions requests Then the server is busy And all slots are busy Then the server is idle And all slots are idle - Then all prompts are predicted \ No newline at end of file + Then all prompts are predicted + From b9f8390d283daaccf75bfb7d310c4293242b20c1 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 22:49:36 +0100 Subject: [PATCH 15/98] server: tests: check for infinite loops --- examples/server/tests/features/server.feature | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index c4d821d74dbfd..35b4244d3b291 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -54,10 +54,6 @@ Feature: llama.cpp server It was her greeting to Prince Vassily, a man high in rank and office, who was the first to arrive at her soirée. """ - And a prompt: - """ - Write another very long music lyrics. - """ Given concurrent completion requests Then the server is busy And all slots are busy @@ -69,7 +65,7 @@ Feature: llama.cpp server Scenario: Multi users OAI Compatibility Given a system prompt "You are an AI assistant." And a model tinyllama-2 - And 512 max tokens to predict + And 1024 max tokens to predict And streaming is enabled Given a prompt: """ @@ -79,10 +75,6 @@ Feature: llama.cpp server """ Write another very long music lyrics. """ - And a prompt: - """ - I believe the meaning of life is - """ Given concurrent OAI completions requests Then the server is busy And all slots are busy From 0772884b06fc9c81db8188fdae3bdfe083cfac54 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 22:55:29 +0100 Subject: [PATCH 16/98] server: tests: add a constant seed in completion request --- examples/server/tests/features/server.feature | 2 +- examples/server/tests/features/steps/steps.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 35b4244d3b291..968e288d084d9 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -1,7 +1,7 @@ Feature: llama.cpp server Background: Server startup - Given a server listening on localhost:8080 with 2 slots + Given a server listening on localhost:8080 with 2 slots and 42 as seed Then the server is starting Then the server is healthy diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 72857c45e145d..400b3c1268f1a 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -7,11 +7,12 @@ from behave import step -@step(u"a server listening on {server_fqdn}:{server_port} with {n_slots} slots") -def step_server_config(context, server_fqdn, server_port, n_slots): +@step(u"a server listening on {server_fqdn}:{server_port} with {n_slots} slots and {seed} as seed") +def step_server_config(context, server_fqdn, server_port, n_slots, seed): context.server_fqdn = server_fqdn context.server_port = int(server_port) context.n_slots = int(n_slots) + context.seed = int(seed) context.base_url = f'http://{context.server_fqdn}:{context.server_port}' context.completions = [] @@ -154,6 +155,7 @@ def request_completion(context, prompt, n_predict=None): response = requests.post(f'{context.base_url}/completion', json={ "prompt": prompt, "n_predict": int(n_predict) if n_predict is not None else 4096, + "seed": context.seed }) assert response.status_code == 200 context.completions.append(response.json()) @@ -173,7 +175,8 @@ def oai_chat_completions(context, user_prompt): ], model=context.model, max_tokens=context.max_tokens, - stream=context.enable_streaming + stream=context.enable_streaming, + seed = context.seed ) if context.enable_streaming: completion_response = { From 6b9dc4f29112402600d91f64142fa1ab3c44fbe0 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 23:05:27 +0100 Subject: [PATCH 17/98] server: tests: add infinite loop --- examples/server/tests/features/server.feature | 43 ++++++++++++------- examples/server/tests/features/steps/steps.py | 6 +-- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 968e288d084d9..6e54395b63aeb 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -36,24 +36,13 @@ Feature: llama.cpp server Scenario: Multi users Given a prompt: """ - Write a formal complaint email to Air France about my delayed - baggage from my flight on Tuesday, January 17th, from Paris to Toulouse. Be verbose. + Write a very long story about AI. """ And a prompt: """ - Translate the following War & Peace chapter into Russian: WELL, PRINCE, - Genoa and Lucca are now no more than private estates of the Bonaparte - family. No, I warn you, that if you do not tell me we are at war, - if you again allow yourself to palliate all the infamies and atrocities - of this Antichrist (upon my word, I believe he is), I don’t know you - in future, you are no longer my friend, no longer my faithful slave, - as you say. There, how do you do, how do you do? I see I’m scaring you, - sit down and talk to me.” These words were uttered in July 1805 by - Anna Pavlovna Scherer, a distinguished lady of the court, - and confidential maid-of-honour to the Empress Marya Fyodorovna. - It was her greeting to Prince Vassily, a man high in rank - and office, who was the first to arrive at her soirée. + Write another very long music lyrics. """ + And 512 max tokens to predict Given concurrent completion requests Then the server is busy And all slots are busy @@ -65,8 +54,6 @@ Feature: llama.cpp server Scenario: Multi users OAI Compatibility Given a system prompt "You are an AI assistant." And a model tinyllama-2 - And 1024 max tokens to predict - And streaming is enabled Given a prompt: """ Write a very long story about AI. @@ -75,6 +62,8 @@ Feature: llama.cpp server """ Write another very long music lyrics. """ + And 512 max tokens to predict + And streaming is enabled Given concurrent OAI completions requests Then the server is busy And all slots are busy @@ -82,3 +71,25 @@ Feature: llama.cpp server And all slots are idle Then all prompts are predicted + # FIXME: infinite loop on the CI, not locally, if n_prompt * n_predict > kv_size + Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size + Given a prompt: + """ + Write a very long story about AI. + """ + And a prompt: + """ + Write another very long music lyrics. + """ + And a prompt: + """ + Write a very long poem. + """ + And 1024 max tokens to predict + Given concurrent completion requests + Then the server is busy + And all slots are busy + Then the server is idle + And all slots are idle + Then all prompts are predicted + diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 400b3c1268f1a..896d8e32d0b73 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -105,7 +105,7 @@ def step_model(context, model): @step(u'{max_tokens} max tokens to predict') def step_max_tokens(context, max_tokens): - context.max_tokens = int(max_tokens) + context.n_predict = int(max_tokens) @step(u'streaming is {enable_streaming}') @@ -154,7 +154,7 @@ def concurrent_requests(context, f_completion): def request_completion(context, prompt, n_predict=None): response = requests.post(f'{context.base_url}/completion', json={ "prompt": prompt, - "n_predict": int(n_predict) if n_predict is not None else 4096, + "n_predict": int(n_predict) if n_predict is not None else context.n_predict, "seed": context.seed }) assert response.status_code == 200 @@ -174,7 +174,7 @@ def oai_chat_completions(context, user_prompt): } ], model=context.model, - max_tokens=context.max_tokens, + max_tokens=context.n_predict, stream=context.enable_streaming, seed = context.seed ) From 68574c6f98b62d22f41efaf33d60999513ee0324 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 23:11:59 +0100 Subject: [PATCH 18/98] server: tests: add infinite loop scenario --- examples/server/tests/features/server.feature | 4 ++-- examples/server/tests/features/steps/steps.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 6e54395b63aeb..681025cf55b64 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -42,7 +42,7 @@ Feature: llama.cpp server """ Write another very long music lyrics. """ - And 512 max tokens to predict + And 256 max tokens to predict Given concurrent completion requests Then the server is busy And all slots are busy @@ -62,7 +62,7 @@ Feature: llama.cpp server """ Write another very long music lyrics. """ - And 512 max tokens to predict + And 256 max tokens to predict And streaming is enabled Given concurrent OAI completions requests Then the server is busy diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 896d8e32d0b73..75e893afab63b 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -176,7 +176,7 @@ def oai_chat_completions(context, user_prompt): model=context.model, max_tokens=context.n_predict, stream=context.enable_streaming, - seed = context.seed + seed=context.seed ) if context.enable_streaming: completion_response = { From b0b6d83c76856bb04a9a0e5260bd4c004b4919b1 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 23:17:00 +0100 Subject: [PATCH 19/98] server: tests: add infinite loop scenario --- examples/server/tests/features/server.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 681025cf55b64..77e8b908806a0 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -42,7 +42,7 @@ Feature: llama.cpp server """ Write another very long music lyrics. """ - And 256 max tokens to predict + And 32 max tokens to predict Given concurrent completion requests Then the server is busy And all slots are busy @@ -62,7 +62,7 @@ Feature: llama.cpp server """ Write another very long music lyrics. """ - And 256 max tokens to predict + And 32 max tokens to predict And streaming is enabled Given concurrent OAI completions requests Then the server is busy From 1ecda0d13eb44793b8926360ad2e29814bdbb86b Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Tue, 20 Feb 2024 23:35:44 +0100 Subject: [PATCH 20/98] server: tests: disable issue 3969 scenario --- examples/server/tests/features/server.feature | 9 +++++++-- examples/server/tests/tests.sh | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 77e8b908806a0..df376b0f21856 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -5,11 +5,13 @@ Feature: llama.cpp server Then the server is starting Then the server is healthy + @llama.cpp Scenario: Health When the server is healthy Then the server is ready And all slots are idle + @llama.cpp Scenario Outline: Completion Given a completion request with maximum tokens Then tokens are predicted @@ -19,6 +21,7 @@ Feature: llama.cpp server | I believe the meaning of life is | 128 | 128 | | Write a joke about AI | 512 | 512 | + @llama.cpp Scenario Outline: OAI Compatibility Given a system prompt And a user prompt @@ -33,6 +36,7 @@ Feature: llama.cpp server | llama-2 | You are ChatGPT. | Say hello. | 64 | false | 64 | | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | true | 512 | + @llama.cpp Scenario: Multi users Given a prompt: """ @@ -50,7 +54,7 @@ Feature: llama.cpp server And all slots are idle Then all prompts are predicted - + @llama.cpp Scenario: Multi users OAI Compatibility Given a system prompt "You are an AI assistant." And a model tinyllama-2 @@ -71,7 +75,8 @@ Feature: llama.cpp server And all slots are idle Then all prompts are predicted - # FIXME: infinite loop on the CI, not locally, if n_prompt * n_predict > kv_size + # FIXME: #3969 infinite loop on the CI, not locally, if n_prompt * n_predict > kv_size + @bug Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size Given a prompt: """ diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index 230ee45add9be..52908b83917b3 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -32,4 +32,4 @@ set -eu "$@" & # Start tests -behave --summary --stop +behave --summary --stop --tags llama.cpp From e6d482088dea57e0615108c914cb0c4e66584e9c Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 00:02:30 +0100 Subject: [PATCH 21/98] server: tests: add embeddings scenario --- examples/server/tests/README.md | 7 ++++- examples/server/tests/features/server.feature | 18 +++++++++++++ examples/server/tests/features/steps/steps.py | 26 ++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 3cdcc5ca3e151..569e675b7bbcd 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -1,6 +1,6 @@ # Server Integration Test -Functional server tests suite. +Server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) with [behave](https://behave.readthedocs.io/en/latest/). ### Install dependencies `pip install -r requirements.txt` @@ -9,3 +9,8 @@ Functional server tests suite. 1. Build the server 2. download a GGUF model: `./scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf` 3. Start the test: `./tests.sh stories260K.gguf -ngl 23` + +### Skipped scenario + +Scenario must be annotated with `@llama.cpp` to be included in the scope. +`@bug` annotation aims to link a scenario with a GitHub issue. \ No newline at end of file diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index df376b0f21856..5f6b161c8865e 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -98,3 +98,21 @@ Feature: llama.cpp server And all slots are idle Then all prompts are predicted + + @llama.cpp + Scenario: Embedding + When embeddings are computed for: + """ + What is the capital of France ? + """ + Then embeddings are generated + + + @llama.cpp + Scenario: OAI Embeddings compatibility + Given a model tinyllama-2 + When an OAI compatible embeddings computation request for: + """ + What is the capital of Spain ? + """ + Then embeddings are generated \ No newline at end of file diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 75e893afab63b..140e0262668a9 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -20,7 +20,6 @@ def step_server_config(context, server_fqdn, server_port, n_slots, seed): context.prompts = [] openai.api_key = 'llama.cpp' - openai.api_base = f'{context.base_url}/v1/chat' @step(u"the server is {expecting_status}") @@ -141,6 +140,30 @@ def step_all_prompts_are_predicted(context): assert_n_tokens_predicted(completion) +@step(u'embeddings are computed for') +def step_compute_embedding(context): + response = requests.post(f'{context.base_url}/embedding', json={ + "content": context.text, + }) + assert response.status_code == 200 + context.embeddings = response.json()['embedding'] + + +@step(u'embeddings are generated') +def step_compute_embeddings(context): + assert len(context.embeddings) > 0 + + +@step(u'an OAI compatible embeddings computation request for') +def step_oai_compute_embedding(context): + openai.api_base = f'{context.base_url}/v1' + embeddings = openai.Embedding.create( + model=context.model, + input=context.text, + ) + context.embeddings = embeddings + + def concurrent_requests(context, f_completion): context.completions.clear() context.completion_threads.clear() @@ -162,6 +185,7 @@ def request_completion(context, prompt, n_predict=None): def oai_chat_completions(context, user_prompt): + openai.api_base = f'{context.base_url}/v1/chat' chat_completion = openai.Completion.create( messages=[ { From 1065f6d41b032b85478fb63837af714b3bbeecf9 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 00:13:53 +0100 Subject: [PATCH 22/98] server: tests: add tokenize/detokenize scenario --- examples/server/tests/README.md | 2 +- examples/server/tests/features/server.feature | 14 ++++++++++-- examples/server/tests/features/steps/steps.py | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 569e675b7bbcd..dc66f660153cc 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -13,4 +13,4 @@ Server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_ ### Skipped scenario Scenario must be annotated with `@llama.cpp` to be included in the scope. -`@bug` annotation aims to link a scenario with a GitHub issue. \ No newline at end of file +`@bug` annotation aims to link a scenario with a GitHub issue. diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 5f6b161c8865e..87a0516f73f0e 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -103,7 +103,7 @@ Feature: llama.cpp server Scenario: Embedding When embeddings are computed for: """ - What is the capital of France ? + What is the capital of Bulgaria ? """ Then embeddings are generated @@ -115,4 +115,14 @@ Feature: llama.cpp server """ What is the capital of Spain ? """ - Then embeddings are generated \ No newline at end of file + Then embeddings are generated + + + @llama.cpp + Scenario: Tokenize / Detokenize + When tokenizing: + """ + What is the capital of France ? + """ + Then tokens can be detokenize + diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 140e0262668a9..1045eeb1f7494 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -164,6 +164,28 @@ def step_oai_compute_embedding(context): context.embeddings = embeddings +@step(u'tokenizing') +def step_tokenize(context): + context.tokenized_text = context.text + response = requests.post(f'{context.base_url}/tokenize', json={ + "content":context.tokenized_text, + }) + assert response.status_code == 200 + context.tokens = response.json()['tokens'] + + +@step(u'tokens can be detokenize') +def step_detokenize(context): + assert len(context.tokens) > 0 + response = requests.post(f'{context.base_url}/detokenize', json={ + "tokens": context.tokens, + }) + assert response.status_code == 200 + print(response.json()) + # FIXME the detokenize answer contains a space prefix ? + assert context.tokenized_text == response.json()['content'].strip() + + def concurrent_requests(context, f_completion): context.completions.clear() context.completion_threads.clear() From 19664b9f0194b40f4234da40ccb4f34df084acf9 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 00:17:38 +0100 Subject: [PATCH 23/98] server: tests: detokenize endpoint issue reference added --- examples/server/tests/features/steps/steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 1045eeb1f7494..e1c69c11b3c0f 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -182,7 +182,7 @@ def step_detokenize(context): }) assert response.status_code == 200 print(response.json()) - # FIXME the detokenize answer contains a space prefix ? + # FIXME the detokenize answer contains a space prefix ? see #3287 assert context.tokenized_text == response.json()['content'].strip() From 6dcbcfe6bab73768afb94347f2e20422b0aed174 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 00:43:50 +0100 Subject: [PATCH 24/98] server: tests: simplify completion scenario --- examples/server/tests/features/server.feature | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 87a0516f73f0e..d6894ae5f6b1b 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -14,12 +14,12 @@ Feature: llama.cpp server @llama.cpp Scenario Outline: Completion Given a completion request with maximum tokens - Then tokens are predicted + Then tokens are predicted Examples: Prompts - | prompt | n_predict | predicted_n | - | I believe the meaning of life is | 128 | 128 | - | Write a joke about AI | 512 | 512 | + | prompt | n_predict | + | I believe the meaning of life is | 128 | + | Write a joke about AI | 512 | @llama.cpp Scenario Outline: OAI Compatibility @@ -29,12 +29,12 @@ Feature: llama.cpp server And max tokens to predict And streaming is Given an OAI compatible chat completions request - Then tokens are predicted + Then tokens are predicted Examples: Prompts - | model | system_prompt | user_prompt | max_tokens | enable_streaming | predicted_n | - | llama-2 | You are ChatGPT. | Say hello. | 64 | false | 64 | - | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | true | 512 | + | model | system_prompt | user_prompt | max_tokens | enable_streaming | + | llama-2 | You are ChatGPT. | Say hello. | 64 | false | + | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | true | @llama.cpp Scenario: Multi users From 672d98f6f0acee9f93bf74e44a032eee5942ff5a Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 01:49:39 +0100 Subject: [PATCH 25/98] server: tests: CORS and api key checks scenario --- examples/server/tests/features/server.feature | 40 ++++-- examples/server/tests/features/steps/steps.py | 135 ++++++++++++------ examples/server/tests/tests.sh | 1 + 3 files changed, 125 insertions(+), 51 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index d6894ae5f6b1b..44c676303ee23 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -1,7 +1,7 @@ Feature: llama.cpp server Background: Server startup - Given a server listening on localhost:8080 with 2 slots and 42 as seed + Given a server listening on localhost:8080 with 2 slots, 42 as seed and llama.cpp as api key Then the server is starting Then the server is healthy @@ -13,13 +13,17 @@ Feature: llama.cpp server @llama.cpp Scenario Outline: Completion - Given a completion request with maximum tokens + Given a prompt + And a user api key + And max tokens to predict + And a completion request Then tokens are predicted Examples: Prompts - | prompt | n_predict | - | I believe the meaning of life is | 128 | - | Write a joke about AI | 512 | + | prompt | n_predict | api_key | + | I believe the meaning of life is | 128 | llama.cpp | + | Write a joke about AI | 512 | llama.cpp | + | say goodbye | 0 | | @llama.cpp Scenario Outline: OAI Compatibility @@ -28,13 +32,15 @@ Feature: llama.cpp server And a model And max tokens to predict And streaming is - Given an OAI compatible chat completions request + And a user api key + Given an OAI compatible chat completions request with an api error Then tokens are predicted Examples: Prompts - | model | system_prompt | user_prompt | max_tokens | enable_streaming | - | llama-2 | You are ChatGPT. | Say hello. | 64 | false | - | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | true | + | model | system_prompt | user_prompt | max_tokens | enable_streaming | api_key | api_error | + | llama-2 | You are ChatGPT. | Say hello. | 64 | false | llama.cpp | none | + | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | true | llama.cpp | none | + | John-Doe | You are an hacker. | Write segfault code in rust. | 0 | true | hackme | raised | @llama.cpp Scenario: Multi users @@ -47,6 +53,7 @@ Feature: llama.cpp server Write another very long music lyrics. """ And 32 max tokens to predict + And a user api key llama.cpp Given concurrent completion requests Then the server is busy And all slots are busy @@ -57,7 +64,7 @@ Feature: llama.cpp server @llama.cpp Scenario: Multi users OAI Compatibility Given a system prompt "You are an AI assistant." - And a model tinyllama-2 + And a model tinyllama-2 Given a prompt: """ Write a very long story about AI. @@ -68,6 +75,7 @@ Feature: llama.cpp server """ And 32 max tokens to predict And streaming is enabled + And a user api key llama.cpp Given concurrent OAI completions requests Then the server is busy And all slots are busy @@ -126,3 +134,15 @@ Feature: llama.cpp server """ Then tokens can be detokenize + @llama.cpp + Scenario Outline: CORS Options + When an OPTIONS request is sent from + Then CORS header is set to + + Examples: Headers + | origin | cors_header | cors_header_value | + | localhost | Access-Control-Allow-Origin | localhost | + | web.mydomain.fr | Access-Control-Allow-Origin | web.mydomain.fr | + | origin | Access-Control-Allow-Credentials | true | + | web.mydomain.fr | Access-Control-Allow-Methods | POST | + | web.mydomain.fr | Access-Control-Allow-Headers | * | diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index e1c69c11b3c0f..edba61777a348 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -7,8 +7,9 @@ from behave import step -@step(u"a server listening on {server_fqdn}:{server_port} with {n_slots} slots and {seed} as seed") -def step_server_config(context, server_fqdn, server_port, n_slots, seed): +@step( + u"a server listening on {server_fqdn}:{server_port} with {n_slots} slots, {seed} as seed and {api_key} as api key") +def step_server_config(context, server_fqdn, server_port, n_slots, seed, api_key): context.server_fqdn = server_fqdn context.server_port = int(server_port) context.n_slots = int(n_slots) @@ -19,7 +20,8 @@ def step_server_config(context, server_fqdn, server_port, n_slots, seed): context.completion_threads = [] context.prompts = [] - openai.api_key = 'llama.cpp' + context.api_key = api_key + openai.api_key = context.api_key @step(u"the server is {expecting_status}") @@ -77,14 +79,16 @@ def step_all_slots_status(context, expected_slot_status_string): request_slots_status(context, expected_slots) -@step(u'a {prompt} completion request with maximum {n_predict} tokens') -def step_request_completion(context, prompt, n_predict): - request_completion(context, prompt, n_predict) +@step(u'a completion request') +def step_request_completion(context): + request_completion(context, context.prompts.pop(), context.n_predict, context.user_api_key) + context.user_api_key = None @step(u'{predicted_n} tokens are predicted') def step_n_tokens_predicted(context, predicted_n): - assert_n_tokens_predicted(context.completions[0], int(predicted_n)) + if int(predicted_n) > 0: + assert_n_tokens_predicted(context.completions[0], int(predicted_n)) @step(u'a user prompt {user_prompt}') @@ -112,9 +116,20 @@ def step_streaming(context, enable_streaming): context.enable_streaming = enable_streaming == 'enabled' or bool(enable_streaming) -@step(u'an OAI compatible chat completions request') -def step_oai_chat_completions(context): - oai_chat_completions(context, context.user_prompt) +@step(u'a user api key {user_api_key}') +def step_user_api_key(context, user_api_key): + context.user_api_key = user_api_key + + +@step(u'a user api key ') +def step_user_api_key(context): + context.user_api_key = None + + +@step(u'an OAI compatible chat completions request with an api error {api_error}') +def step_oai_chat_completions(context, api_error): + oai_chat_completions(context, context.user_prompt, api_error=api_error == 'raised') + context.user_api_key = None @step(u'a prompt') @@ -122,14 +137,19 @@ def step_a_prompt(context): context.prompts.append(context.text) +@step(u'a prompt {prompt}') +def step_a_prompt_prompt(context, prompt): + context.prompts.append(prompt) + + @step(u'concurrent completion requests') def step_concurrent_completion_requests(context): - concurrent_requests(context, request_completion) + concurrent_requests(context, request_completion, context.n_predict, context.user_api_key) @step(u'concurrent OAI completions requests') def step_oai_chat_completions(context): - concurrent_requests(context, oai_chat_completions) + concurrent_requests(context, oai_chat_completions, context.user_api_key) @step(u'all prompts are predicted') @@ -168,7 +188,7 @@ def step_oai_compute_embedding(context): def step_tokenize(context): context.tokenized_text = context.text response = requests.post(f'{context.base_url}/tokenize', json={ - "content":context.tokenized_text, + "content": context.tokenized_text, }) assert response.status_code == 200 context.tokens = response.json()['tokens'] @@ -181,49 +201,82 @@ def step_detokenize(context): "tokens": context.tokens, }) assert response.status_code == 200 - print(response.json()) # FIXME the detokenize answer contains a space prefix ? see #3287 assert context.tokenized_text == response.json()['content'].strip() -def concurrent_requests(context, f_completion): +@step(u'an OPTIONS request is sent from {origin}') +def step_options_request(context, origin): + options_response = requests.options(f'{context.base_url}/v1/chat/completions', + headers={"Origin": origin}) + assert options_response.status_code == 200 + context.options_response = options_response + + +@step(u'CORS header {cors_header} is set to {cors_header_value}') +def step_check_options_header_value(context, cors_header, cors_header_value): + assert context.options_response.headers[cors_header] == cors_header_value + + +def concurrent_requests(context, f_completion, *argv): context.completions.clear() context.completion_threads.clear() for prompt in context.prompts: - completion_thread = threading.Thread(target=f_completion, args=(context, prompt)) + completion_thread = threading.Thread(target=f_completion, args=(context, prompt, *argv)) completion_thread.start() context.completion_threads.append(completion_thread) context.prompts.clear() -def request_completion(context, prompt, n_predict=None): - response = requests.post(f'{context.base_url}/completion', json={ - "prompt": prompt, - "n_predict": int(n_predict) if n_predict is not None else context.n_predict, - "seed": context.seed - }) - assert response.status_code == 200 - context.completions.append(response.json()) +def request_completion(context, prompt, n_predict=None, user_api_key=None): + origin = "my.super.domain" + headers = { + 'Origin': origin + } + if 'user_api_key' in context: + headers['Authorization'] = f'Bearer {user_api_key}' + + response = requests.post(f'{context.base_url}/completion', + json={ + "prompt": prompt, + "n_predict": int(n_predict) if n_predict is not None else context.n_predict, + "seed": context.seed + }, + headers=headers) + if n_predict is not None and n_predict > 0: + assert response.status_code == 200 + assert response.headers['Access-Control-Allow-Origin'] == origin + context.completions.append(response.json()) + else: + assert response.status_code == 401 -def oai_chat_completions(context, user_prompt): + +def oai_chat_completions(context, user_prompt, api_error=None): + openai.api_key = context.user_api_key openai.api_base = f'{context.base_url}/v1/chat' - chat_completion = openai.Completion.create( - messages=[ - { - "role": "system", - "content": context.system_prompt, - }, - { - "role": "user", - "content": user_prompt, - } - ], - model=context.model, - max_tokens=context.n_predict, - stream=context.enable_streaming, - seed=context.seed - ) + try: + chat_completion = openai.Completion.create( + messages=[ + { + "role": "system", + "content": context.system_prompt, + }, + { + "role": "user", + "content": user_prompt, + } + ], + model=context.model, + max_tokens=context.n_predict, + stream=context.enable_streaming, + seed=context.seed + ) + except openai.error.APIError: + if api_error: + openai.api_key = context.api_key + return + openai.api_key = context.api_key if context.enable_streaming: completion_response = { 'content': '', diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index 52908b83917b3..19cd7f17b3b23 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -29,6 +29,7 @@ set -eu --threads-batch 4 \ --embedding \ --cont-batching \ + --api-key llama.cpp \ "$@" & # Start tests From 3322bfa9807ef248915af1e5278e2780bfaa4645 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 02:04:59 +0100 Subject: [PATCH 26/98] server: tests: add a small check to be sure all started threads have generated response --- examples/server/tests/features/steps/steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index edba61777a348..755c804975e75 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -156,6 +156,7 @@ def step_oai_chat_completions(context): def step_all_prompts_are_predicted(context): for completion_thread in context.completion_threads: completion_thread.join() + assert len(context.completions) == len(context.completion_threads) for completion in context.completions: assert_n_tokens_predicted(completion) @@ -251,7 +252,6 @@ def request_completion(context, prompt, n_predict=None, user_api_key=None): assert response.status_code == 401 - def oai_chat_completions(context, user_prompt, api_error=None): openai.api_key = context.user_api_key openai.api_base = f'{context.base_url}/v1/chat' From 469af4b4ec175a5068aaea4edbac45aa6bd1736a Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 02:20:44 +0100 Subject: [PATCH 27/98] server: tests: change CI workflow trigger --- .github/workflows/server-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index b70006e04a4be..15dc6d683cb50 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -1,13 +1,15 @@ # Server test scenario name: Server Integration Tests -# FIXME put only necessary triggers on: push: branches: - master - test/server-add-ci-test # FIXME remove paths: ['.github/workflows/server-test.yml', '**/CMakeLists.txt', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', 'examples/server/**.*'] + pull_request: + types: [opened, synchronize, reopened] + paths: ['.github/workflows/server-test.yml', '**/CMakeLists.txt', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', 'examples/server/**.*'] jobs: ubuntu-latest-cmake: From 2a37bd6b860013902825b40ffcf9321d3a874d93 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 02:29:50 +0100 Subject: [PATCH 28/98] server: tests: fix the multi users infinite loop test --- examples/server/tests/features/server.feature | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 44c676303ee23..8bafbc39b7519 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -98,7 +98,12 @@ Feature: llama.cpp server """ Write a very long poem. """ - And 1024 max tokens to predict + And a prompt: + """ + Write a very long joke. + """ + And 512 max tokens to predict + And a user api key llama.cpp Given concurrent completion requests Then the server is busy And all slots are busy From f1d4138c1389d7befad88493fc77f41634c9a770 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Wed, 21 Feb 2024 13:08:57 +0200 Subject: [PATCH 29/98] server : fix initialization thread issues --- examples/server/server.cpp | 26 +++++++++---------- examples/server/tests/features/server.feature | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/server/server.cpp b/examples/server/server.cpp index c7821eca68cba..b77e00a4d5bc8 100644 --- a/examples/server/server.cpp +++ b/examples/server/server.cpp @@ -2719,19 +2719,6 @@ int main(int argc, char **argv) log_data["api_key"] = "api_key: " + std::to_string(sparams.api_keys.size()) + " keys loaded"; } - LOG_INFO("HTTP server listening", log_data); - // run the HTTP server in a thread - see comment below - std::thread t([&]() - { - if (!svr.listen_after_bind()) - { - state.store(SERVER_STATE_ERROR); - return 1; - } - - return 0; - }); - // load the model if (!llama.load_model(params)) { @@ -3194,6 +3181,19 @@ int main(int argc, char **argv) }*/ //); + LOG_INFO("HTTP server listening", log_data); + // run the HTTP server in a thread - see comment below + std::thread t([&]() + { + if (!svr.listen_after_bind()) + { + state.store(SERVER_STATE_ERROR); + return 1; + } + + return 0; + }); + llama.queue_tasks.on_new_task(std::bind( &llama_server_context::process_single_task, &llama, std::placeholders::_1)); llama.queue_tasks.on_finish_multitask(std::bind( diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index 8bafbc39b7519..f06375c25f725 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -84,7 +84,7 @@ Feature: llama.cpp server Then all prompts are predicted # FIXME: #3969 infinite loop on the CI, not locally, if n_prompt * n_predict > kv_size - @bug + @llama.cpp Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size Given a prompt: """ From 600cbeb7eb27ffb9effbe38be75a1d50c8011b8c Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 18:35:21 +0100 Subject: [PATCH 30/98] server: test: ci change the GitHub workflow trigger --- .github/workflows/server-test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 15dc6d683cb50..be3fbd4dc5a79 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -2,14 +2,15 @@ name: Server Integration Tests on: + workflow_dispatch: # allows manual triggering push: branches: - master - test/server-add-ci-test # FIXME remove - paths: ['.github/workflows/server-test.yml', '**/CMakeLists.txt', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', 'examples/server/**.*'] + paths: ['.github/workflows/**', '**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp'] pull_request: types: [opened, synchronize, reopened] - paths: ['.github/workflows/server-test.yml', '**/CMakeLists.txt', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', 'examples/server/**.*'] + paths: ['**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp'] jobs: ubuntu-latest-cmake: From 6406208174bb6a7bc098a5915442eb1c7126825a Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 22:13:37 +0100 Subject: [PATCH 31/98] server: tests: * start the server at each scenario * split the features as each requires different server config --- examples/server/tests/README.md | 9 +- examples/server/tests/features/environment.py | 4 + .../server/tests/features/security.feature | 49 ++++++ examples/server/tests/features/server.feature | 128 +++------------ examples/server/tests/features/steps/steps.py | 152 +++++++++++++----- examples/server/tests/tests.sh | 28 +--- 6 files changed, 197 insertions(+), 173 deletions(-) create mode 100644 examples/server/tests/features/environment.py create mode 100644 examples/server/tests/features/security.feature diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index dc66f660153cc..5e5da6ff8fa10 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -7,10 +7,13 @@ Server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_ ### Run tests 1. Build the server -2. download a GGUF model: `./scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf` -3. Start the test: `./tests.sh stories260K.gguf -ngl 23` +2. download required models: + 1. `../../../scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf` +3. Start the test: `./tests.sh` + +To change the server path, use `LLAMA_SERVER_BIN_PATH` environment variable. ### Skipped scenario -Scenario must be annotated with `@llama.cpp` to be included in the scope. +Feature or Scenario must be annotated with `@llama.cpp` to be included in the scope. `@bug` annotation aims to link a scenario with a GitHub issue. diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py new file mode 100644 index 0000000000000..c25b75cf7dbde --- /dev/null +++ b/examples/server/tests/features/environment.py @@ -0,0 +1,4 @@ + +def after_scenario(context, scenario): + print("stopping server...") + context.server_process.kill() diff --git a/examples/server/tests/features/security.feature b/examples/server/tests/features/security.feature new file mode 100644 index 0000000000000..2bfcf956a8547 --- /dev/null +++ b/examples/server/tests/features/security.feature @@ -0,0 +1,49 @@ +@llama.cpp +Feature: Security + + Background: Server startup with an api key defined + Given a server listening on localhost:8080 + And a model file stories260K.gguf + And a server api key llama.cpp + Then the server is starting + + Scenario Outline: Completion with some user api key + Given a prompt test + And a user api key + And 4 max tokens to predict + And a completion request with api error + + Examples: Prompts + | api_key | api_error | + | llama.cpp | no | + | llama.cpp | no | + | hackeme | raised | + | | raised | + + Scenario Outline: OAI Compatibility + Given a system prompt test + And a user prompt test + And a model test + And 2 max tokens to predict + And streaming is disabled + And a user api key + Given an OAI compatible chat completions request with api error + + Examples: Prompts + | api_key | api_error | + | llama.cpp | no | + | llama.cpp | no | + | hackme | raised | + + + Scenario Outline: CORS Options + When an OPTIONS request is sent from + Then CORS header is set to + + Examples: Headers + | origin | cors_header | cors_header_value | + | localhost | Access-Control-Allow-Origin | localhost | + | web.mydomain.fr | Access-Control-Allow-Origin | web.mydomain.fr | + | origin | Access-Control-Allow-Credentials | true | + | web.mydomain.fr | Access-Control-Allow-Methods | POST | + | web.mydomain.fr | Access-Control-Allow-Headers | * | \ No newline at end of file diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index f06375c25f725..ea0079516d1ef 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -1,118 +1,46 @@ +@llama.cpp Feature: llama.cpp server Background: Server startup - Given a server listening on localhost:8080 with 2 slots, 42 as seed and llama.cpp as api key + Given a server listening on localhost:8080 + And a model file stories260K.gguf + And a model alias tinyllama-2 + And 42 as server seed + And 32 KV cache size + And 1 slots + And 32 server max tokens to predict Then the server is starting Then the server is healthy - @llama.cpp Scenario: Health - When the server is healthy Then the server is ready And all slots are idle - @llama.cpp Scenario Outline: Completion Given a prompt - And a user api key And max tokens to predict - And a completion request - Then tokens are predicted + And a completion request with no api error + Then tokens are predicted with content: Examples: Prompts - | prompt | n_predict | api_key | - | I believe the meaning of life is | 128 | llama.cpp | - | Write a joke about AI | 512 | llama.cpp | - | say goodbye | 0 | | + | prompt | n_predict | content | n_predicted | + | I believe the meaning of life is | 8 | going to read. | 8 | + | Write a joke about AI | 64 | tion came to the park. And all his friends were very scared and did not | 32 | - @llama.cpp Scenario Outline: OAI Compatibility - Given a system prompt + Given a model + And a system prompt And a user prompt - And a model And max tokens to predict And streaming is - And a user api key - Given an OAI compatible chat completions request with an api error - Then tokens are predicted + Given an OAI compatible chat completions request with no api error + Then tokens are predicted with content: Examples: Prompts - | model | system_prompt | user_prompt | max_tokens | enable_streaming | api_key | api_error | - | llama-2 | You are ChatGPT. | Say hello. | 64 | false | llama.cpp | none | - | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 512 | true | llama.cpp | none | - | John-Doe | You are an hacker. | Write segfault code in rust. | 0 | true | hackme | raised | + | model | system_prompt | user_prompt | max_tokens | content | n_predicted | enable_streaming | + | llama-2 | Book | What is the best book | 8 | "Mom, what' | 8 | disabled | + | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 64 | "Hey," said the bird.The bird was very happy and thanked the bird for hel | 32 | enabled | - @llama.cpp - Scenario: Multi users - Given a prompt: - """ - Write a very long story about AI. - """ - And a prompt: - """ - Write another very long music lyrics. - """ - And 32 max tokens to predict - And a user api key llama.cpp - Given concurrent completion requests - Then the server is busy - And all slots are busy - Then the server is idle - And all slots are idle - Then all prompts are predicted - - @llama.cpp - Scenario: Multi users OAI Compatibility - Given a system prompt "You are an AI assistant." - And a model tinyllama-2 - Given a prompt: - """ - Write a very long story about AI. - """ - And a prompt: - """ - Write another very long music lyrics. - """ - And 32 max tokens to predict - And streaming is enabled - And a user api key llama.cpp - Given concurrent OAI completions requests - Then the server is busy - And all slots are busy - Then the server is idle - And all slots are idle - Then all prompts are predicted - - # FIXME: #3969 infinite loop on the CI, not locally, if n_prompt * n_predict > kv_size - @llama.cpp - Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size - Given a prompt: - """ - Write a very long story about AI. - """ - And a prompt: - """ - Write another very long music lyrics. - """ - And a prompt: - """ - Write a very long poem. - """ - And a prompt: - """ - Write a very long joke. - """ - And 512 max tokens to predict - And a user api key llama.cpp - Given concurrent completion requests - Then the server is busy - And all slots are busy - Then the server is idle - And all slots are idle - Then all prompts are predicted - - - @llama.cpp Scenario: Embedding When embeddings are computed for: """ @@ -120,8 +48,6 @@ Feature: llama.cpp server """ Then embeddings are generated - - @llama.cpp Scenario: OAI Embeddings compatibility Given a model tinyllama-2 When an OAI compatible embeddings computation request for: @@ -131,23 +57,9 @@ Feature: llama.cpp server Then embeddings are generated - @llama.cpp Scenario: Tokenize / Detokenize When tokenizing: """ What is the capital of France ? """ Then tokens can be detokenize - - @llama.cpp - Scenario Outline: CORS Options - When an OPTIONS request is sent from - Then CORS header is set to - - Examples: Headers - | origin | cors_header | cors_header_value | - | localhost | Access-Control-Allow-Origin | localhost | - | web.mydomain.fr | Access-Control-Allow-Origin | web.mydomain.fr | - | origin | Access-Control-Allow-Credentials | true | - | web.mydomain.fr | Access-Control-Allow-Methods | POST | - | web.mydomain.fr | Access-Control-Allow-Headers | * | diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 755c804975e75..db5d4b829ad5d 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -1,4 +1,6 @@ +import os import socket +import subprocess import threading from contextlib import closing @@ -8,26 +10,62 @@ @step( - u"a server listening on {server_fqdn}:{server_port} with {n_slots} slots, {seed} as seed and {api_key} as api key") -def step_server_config(context, server_fqdn, server_port, n_slots, seed, api_key): + u"a server listening on {server_fqdn}:{server_port}") +def step_server_config(context, server_fqdn, server_port): context.server_fqdn = server_fqdn context.server_port = int(server_port) - context.n_slots = int(n_slots) - context.seed = int(seed) + context.base_url = f'http://{context.server_fqdn}:{context.server_port}' + context.model_alias = None + context.n_ctx = None + context.n_predict = None + context.n_server_predict = None + context.n_slots = None + context.server_api_key = None + context.server_seed = None + context.user_api_key = None + context.completions = [] context.completion_threads = [] context.prompts = [] - context.api_key = api_key - openai.api_key = context.api_key + +@step(u'a model file {model_file}') +def step_model_file(context, model_file): + context.model_file = model_file + + +@step(u'a model alias {model_alias}') +def step_model_alias(context, model_alias): + context.model_alias = model_alias + + +@step(u'{seed} as server seed') +def step_seed(context, seed): + context.server_seed = int(seed) + + +@step(u'{n_ctx} KV cache size') +def step_n_ctx(context, n_ctx): + context.n_ctx = int(n_ctx) + + +@step(u'{n_slots} slots') +def step_n_slots(context, n_slots): + context.n_slots = int(n_slots) + + +@step(u'{n_predict} server max tokens to predict') +def step_server_n_predict(context, n_predict): + context.n_server_predict = int(n_predict) @step(u"the server is {expecting_status}") def step_wait_for_the_server_to_be_started(context, expecting_status): match expecting_status: case 'starting': + start_server_background(context) server_started = False while not server_started: with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: @@ -43,19 +81,13 @@ def step_wait_for_the_server_to_be_started(context, expecting_status): params={'fail_on_no_slot': True}, slots_idle=context.n_slots, slots_processing=0) - request_slots_status(context, [ - {'id': 0, 'state': 0}, - {'id': 1, 'state': 0} - ]) + request_slots_status(context, [{'id': slot_id, 'state': 0} for slot_id in range(context.n_slots)]) case 'busy': wait_for_health_status(context, 503, 'no slot available', params={'fail_on_no_slot': True}, slots_idle=0, slots_processing=context.n_slots) - request_slots_status(context, [ - {'id': 0, 'state': 1}, - {'id': 1, 'state': 1} - ]) + request_slots_status(context, [{'id': slot_id, 'state': 1} for slot_id in range(context.n_slots)]) case _: assert False, "unknown status" @@ -79,10 +111,16 @@ def step_all_slots_status(context, expected_slot_status_string): request_slots_status(context, expected_slots) -@step(u'a completion request') -def step_request_completion(context): - request_completion(context, context.prompts.pop(), context.n_predict, context.user_api_key) - context.user_api_key = None +@step(u'a completion request with {api_error} api error') +def step_request_completion(context, api_error): + request_completion(context, context.prompts.pop(), + n_predict=context.n_predict, + expect_api_error=api_error == 'raised') + + +@step(u'{predicted_n} tokens are predicted with content: {content}') +def step_n_tokens_predicted_with_content(context, predicted_n, content): + assert_n_tokens_predicted(context.completions[0], int(predicted_n), content) @step(u'{predicted_n} tokens are predicted') @@ -122,14 +160,23 @@ def step_user_api_key(context, user_api_key): @step(u'a user api key ') -def step_user_api_key(context): +def step_no_user_api_key(context): context.user_api_key = None -@step(u'an OAI compatible chat completions request with an api error {api_error}') +@step(u'no user api key') +def step_no_user_api_key(context): + context.user_api_key = None + + +@step(u'a server api key {server_api_key}') +def step_server_api_key(context, server_api_key): + context.server_api_key = server_api_key + + +@step(u'an OAI compatible chat completions request with {api_error} api error') def step_oai_chat_completions(context, api_error): oai_chat_completions(context, context.user_prompt, api_error=api_error == 'raised') - context.user_api_key = None @step(u'a prompt') @@ -144,12 +191,12 @@ def step_a_prompt_prompt(context, prompt): @step(u'concurrent completion requests') def step_concurrent_completion_requests(context): - concurrent_requests(context, request_completion, context.n_predict, context.user_api_key) + concurrent_requests(context, request_completion) @step(u'concurrent OAI completions requests') def step_oai_chat_completions(context): - concurrent_requests(context, oai_chat_completions, context.user_api_key) + concurrent_requests(context, oai_chat_completions) @step(u'all prompts are predicted') @@ -177,6 +224,9 @@ def step_compute_embeddings(context): @step(u'an OAI compatible embeddings computation request for') def step_oai_compute_embedding(context): + openai.api_key = 'nope' # openai client always expects an api_keu + if context.user_api_key is not None: + openai.api_key = context.user_api_key openai.api_base = f'{context.base_url}/v1' embeddings = openai.Embedding.create( model=context.model, @@ -202,7 +252,7 @@ def step_detokenize(context): "tokens": context.tokens, }) assert response.status_code == 200 - # FIXME the detokenize answer contains a space prefix ? see #3287 + # SPM tokenizer adds a whitespace prefix: https://github.com/google/sentencepiece/issues/15 assert context.tokenized_text == response.json()['content'].strip() @@ -229,22 +279,23 @@ def concurrent_requests(context, f_completion, *argv): context.prompts.clear() -def request_completion(context, prompt, n_predict=None, user_api_key=None): +def request_completion(context, prompt, n_predict=None, expect_api_error=None): origin = "my.super.domain" headers = { 'Origin': origin } - if 'user_api_key' in context: - headers['Authorization'] = f'Bearer {user_api_key}' + if context.user_api_key is not None: + print(f"Set user_api_key: {context.user_api_key}") + headers['Authorization'] = f'Bearer {context.user_api_key}' response = requests.post(f'{context.base_url}/completion', json={ "prompt": prompt, "n_predict": int(n_predict) if n_predict is not None else context.n_predict, - "seed": context.seed + "seed": context.server_seed if context.server_seed is not None else 42 }, headers=headers) - if n_predict is not None and n_predict > 0: + if expect_api_error is not None and not expect_api_error: assert response.status_code == 200 assert response.headers['Access-Control-Allow-Origin'] == origin context.completions.append(response.json()) @@ -253,7 +304,9 @@ def request_completion(context, prompt, n_predict=None, user_api_key=None): def oai_chat_completions(context, user_prompt, api_error=None): - openai.api_key = context.user_api_key + openai.api_key = 'nope' # openai client always expects an api_keu + if context.user_api_key is not None: + openai.api_key = context.user_api_key openai.api_base = f'{context.base_url}/v1/chat' try: chat_completion = openai.Completion.create( @@ -270,13 +323,11 @@ def oai_chat_completions(context, user_prompt, api_error=None): model=context.model, max_tokens=context.n_predict, stream=context.enable_streaming, - seed=context.seed + seed=context.server_seed if context.server_seed is not None else 42 ) except openai.error.APIError: - if api_error: - openai.api_key = context.api_key + if api_error is not None and api_error: return - openai.api_key = context.api_key if context.enable_streaming: completion_response = { 'content': '', @@ -301,13 +352,17 @@ def oai_chat_completions(context, user_prompt, api_error=None): }) -def assert_n_tokens_predicted(completion_response, expected_predicted_n=None): +def assert_n_tokens_predicted(completion_response, expected_predicted_n=None, expected_content=None): content = completion_response['content'] n_predicted = completion_response['timings']['predicted_n'] assert len(content) > 0, "no token predicted" if expected_predicted_n is not None: assert n_predicted == expected_predicted_n, (f'invalid number of tokens predicted:' - f' "{n_predicted}" <> "{expected_predicted_n}"') + f' {n_predicted} <> {expected_predicted_n}') + if expected_content is not None: + expected_content = expected_content.replace('', ' ').replace('', '\n') + assert content == expected_content, (f'invalid tokens predicted:' + f' ```\n{content}\n``` <> ```\n{expected_content}\n```') def wait_for_health_status(context, expected_http_status_code, @@ -334,3 +389,28 @@ def request_slots_status(context, expected_slots): for expected, slot in zip(expected_slots, slots): for key in expected: assert expected[key] == slot[key], f"expected[{key}] != slot[{key}]" + + +def start_server_background(context): + context.server_path = '../../../build/bin/server' + if 'LLAMA_SERVER_BIN_PATH' in os.environ: + context.server_path = os.environ['LLAMA_SERVER_BIN_PATH'] + server_args = [ + '--model', context.model_file + ] + if context.model_alias is not None: + server_args.extend(['--alias', context.model_alias]) + if context.server_seed is not None: + server_args.extend(['--alias', context.model_alias]) + if context.n_ctx is not None: + server_args.extend(['--ctx-size', context.n_ctx]) + if context.n_slots is not None: + server_args.extend(['--parallel', context.n_slots]) + if context.n_server_predict is not None: + server_args.extend(['--n-predict', context.n_server_predict]) + if context.server_api_key is not None: + server_args.extend(['--api-key', context.server_api_key]) + print(f"starting server with: {context.server_path}", *server_args) + context.server_process = subprocess.Popen( + [str(arg) for arg in [context.server_path, *server_args]], + close_fds=True) diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index 19cd7f17b3b23..ff3e94306304a 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -1,36 +1,12 @@ #!/bin/bash -if [ $# -lt 1 ] -then - >&2 echo "Usage: $0 model_path [server_args...]" - exit 1 -fi - -# kill the server at the end +# kill any dandling server at the end cleanup() { pkill -P $$ } trap cleanup EXIT -model_path="$1" -shift 1 - set -eu -# Start the server in background -../../../build/bin/server \ - --model "$model_path" \ - --alias tinyllama-2 \ - --ctx-size 1024 \ - --parallel 2 \ - --n-predict 1024 \ - --batch-size 32 \ - --threads 4 \ - --threads-batch 4 \ - --embedding \ - --cont-batching \ - --api-key llama.cpp \ - "$@" & - -# Start tests +# Start @llama.cpp scenario behave --summary --stop --tags llama.cpp From 01cca6625bec7504b25578f67fd0cfe28b709c33 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 22:43:39 +0100 Subject: [PATCH 32/98] server: tests: ci fix model download path --- .github/workflows/server-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index be3fbd4dc5a79..3eee2c2ce364d 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -43,7 +43,8 @@ jobs: - name: Download test model id: download_model run: | - ./scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf + cd examples/server/tests + ../../../scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf - name: Server Integration Tests id: server_integration_test From 534998dbb9e03705159afd14a2501817ca06bd5c Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Wed, 21 Feb 2024 23:06:20 +0100 Subject: [PATCH 33/98] server: tests: ci tests.sh exit code --- examples/server/tests/features/environment.py | 4 ++++ examples/server/tests/features/security.feature | 1 + examples/server/tests/tests.sh | 6 ------ 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index c25b75cf7dbde..cae6ea8bda353 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -1,4 +1,8 @@ +import time + def after_scenario(context, scenario): print("stopping server...") context.server_process.kill() + # Wait few for socket to be free up + time.sleep(0.05) diff --git a/examples/server/tests/features/security.feature b/examples/server/tests/features/security.feature index 2bfcf956a8547..dc96c865a075a 100644 --- a/examples/server/tests/features/security.feature +++ b/examples/server/tests/features/security.feature @@ -6,6 +6,7 @@ Feature: Security And a model file stories260K.gguf And a server api key llama.cpp Then the server is starting + Then the server is healthy Scenario Outline: Completion with some user api key Given a prompt test diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index ff3e94306304a..7d43ddfc8cd68 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -1,11 +1,5 @@ #!/bin/bash -# kill any dandling server at the end -cleanup() { - pkill -P $$ -} -trap cleanup EXIT - set -eu # Start @llama.cpp scenario From a697cd1314268d42d6d6cfaf0f6a5185fa8f1d9b Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Thu, 22 Feb 2024 13:29:20 +0200 Subject: [PATCH 34/98] minor : fix missing new line --- examples/server/tests/features/security.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/server/tests/features/security.feature b/examples/server/tests/features/security.feature index dc96c865a075a..678c4d948fba6 100644 --- a/examples/server/tests/features/security.feature +++ b/examples/server/tests/features/security.feature @@ -47,4 +47,5 @@ Feature: Security | web.mydomain.fr | Access-Control-Allow-Origin | web.mydomain.fr | | origin | Access-Control-Allow-Credentials | true | | web.mydomain.fr | Access-Control-Allow-Methods | POST | - | web.mydomain.fr | Access-Control-Allow-Headers | * | \ No newline at end of file + | web.mydomain.fr | Access-Control-Allow-Headers | * | + From 41676d992047620049592ee5d4e97eca040e16e1 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Thu, 22 Feb 2024 13:33:00 +0200 Subject: [PATCH 35/98] ci : actually no reason to exclude GPU code from triggers --- .github/workflows/server-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 3eee2c2ce364d..036543806c5c5 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -7,10 +7,10 @@ on: branches: - master - test/server-add-ci-test # FIXME remove - paths: ['.github/workflows/**', '**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp'] + paths: ['.github/workflows/**', '**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m'] pull_request: types: [opened, synchronize, reopened] - paths: ['**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp'] + paths: ['**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m'] jobs: ubuntu-latest-cmake: From 016b2215495070e21ab6718fc72f87b6ab4cbe14 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Thu, 22 Feb 2024 21:47:51 +0100 Subject: [PATCH 36/98] server: fix health/slots endpoint slot state access available race condition --- examples/server/server.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/server/server.cpp b/examples/server/server.cpp index 020cc2797a9b8..8a62f18e41ee7 100644 --- a/examples/server/server.cpp +++ b/examples/server/server.cpp @@ -1400,11 +1400,6 @@ struct llama_server_context int n_processing_slots = 0; for (llama_client_slot &slot: slots) { - if (slot.available()) { - n_idle_slots++; - } else { - n_processing_slots++; - } json slot_data = get_formated_generation(slot); slot_data["id"] = slot.id; slot_data["task_id"] = slot.task_id; @@ -1419,6 +1414,11 @@ struct llama_server_context {"stopped_limit", slot.stopped_limit}, {"stopping_word", slot.stopping_word}, }; + if (slot_data["state"] == IDLE) { + n_idle_slots++; + } else { + n_processing_slots++; + } slots_data.push_back(slot_data); } LOG_TEE("task %i - slots data: idle=%i processing=%i\n", task.id, n_idle_slots, n_processing_slots); From e43406e36dea0600c6b3400dcffbb7336600661a Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Thu, 22 Feb 2024 21:55:00 +0100 Subject: [PATCH 37/98] server: tests: switch to asyncio for concurrent tests, match result content with regex --- .../server/tests/features/parallel.feature | 54 ++ .../server/tests/features/security.feature | 1 - examples/server/tests/features/server.feature | 16 +- examples/server/tests/features/steps/steps.py | 502 ++++++++++++------ examples/server/tests/requirements.txt | 1 + examples/server/tests/tests.sh | 2 +- 6 files changed, 404 insertions(+), 172 deletions(-) create mode 100644 examples/server/tests/features/parallel.feature diff --git a/examples/server/tests/features/parallel.feature b/examples/server/tests/features/parallel.feature new file mode 100644 index 0000000000000..b420a8501d710 --- /dev/null +++ b/examples/server/tests/features/parallel.feature @@ -0,0 +1,54 @@ +@llama.cpp +Feature: Parallel + + Background: Server startup + Given a server listening on localhost:8080 + And a model file stories260K.gguf + And a model alias tinyllama-2 + And 42 as server seed + And 32 KV cache size + And 2 slots + And continuous batching + Then the server is starting + Then the server is healthy + + Scenario Outline: Multi users completion + Given a prompt: + """ + Write a very long story about AI. + """ + And a prompt: + """ + Write another very long music lyrics. + """ + And max tokens to predict + Given concurrent completion requests + Then the server is busy + Then the server is idle + And all slots are idle + Then all prompts are predicted with tokens + Examples: + | n_predict | + | 512 | + + Scenario Outline: Multi users OAI completions compatibility + Given a system prompt You are a writer. + And a model tinyllama-2 + Given a prompt: + """ + Write a very long book. + """ + And a prompt: + """ + Write another a poem. + """ + And max tokens to predict + And streaming is + Given concurrent OAI completions requests + Then the server is busy + Then the server is idle + Then all prompts are predicted with tokens + Examples: + | streaming | n_predict | + | disabled | 512 | + #| enabled | 512 | FIXME: phymbert: need to investigate why in aiohttp with streaming only one token is generated diff --git a/examples/server/tests/features/security.feature b/examples/server/tests/features/security.feature index 678c4d948fba6..db06d39775c05 100644 --- a/examples/server/tests/features/security.feature +++ b/examples/server/tests/features/security.feature @@ -48,4 +48,3 @@ Feature: Security | origin | Access-Control-Allow-Credentials | true | | web.mydomain.fr | Access-Control-Allow-Methods | POST | | web.mydomain.fr | Access-Control-Allow-Headers | * | - diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index ea0079516d1ef..c36b42e07d7f7 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -20,12 +20,12 @@ Feature: llama.cpp server Given a prompt And max tokens to predict And a completion request with no api error - Then tokens are predicted with content: + Then tokens are predicted matching Examples: Prompts - | prompt | n_predict | content | n_predicted | - | I believe the meaning of life is | 8 | going to read. | 8 | - | Write a joke about AI | 64 | tion came to the park. And all his friends were very scared and did not | 32 | + | prompt | n_predict | re_content | n_predicted | + | I believe the meaning of life is | 8 | read | 8 | + | Write a joke about AI | 64 | (parkfriendsscared)+ | 32 | Scenario Outline: OAI Compatibility Given a model @@ -34,12 +34,12 @@ Feature: llama.cpp server And max tokens to predict And streaming is Given an OAI compatible chat completions request with no api error - Then tokens are predicted with content: + Then tokens are predicted matching Examples: Prompts - | model | system_prompt | user_prompt | max_tokens | content | n_predicted | enable_streaming | - | llama-2 | Book | What is the best book | 8 | "Mom, what' | 8 | disabled | - | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 64 | "Hey," said the bird.The bird was very happy and thanked the bird for hel | 32 | enabled | + | model | system_prompt | user_prompt | max_tokens | re_content | n_predicted | enable_streaming | + | llama-2 | Book | What is the best book | 8 | (Momwhat)+ | 8 | disabled | + | codellama70b | You are a coding assistant. | Write the fibonacci function in c++. | 64 | (thankshappybird)+ | 32 | enabled | Scenario: Embedding When embeddings are computed for: diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index db5d4b829ad5d..1e27ce274d2f4 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -1,22 +1,27 @@ +import asyncio +import json import os +import re import socket import subprocess -import threading from contextlib import closing +from re import RegexFlag +import aiohttp import openai import requests from behave import step +from behave.api.async_step import async_run_until_complete -@step( - u"a server listening on {server_fqdn}:{server_port}") +@step(u"a server listening on {server_fqdn}:{server_port}") def step_server_config(context, server_fqdn, server_port): context.server_fqdn = server_fqdn context.server_port = int(server_port) context.base_url = f'http://{context.server_fqdn}:{context.server_port}' + context.server_continuous_batching = False context.model_alias = None context.n_ctx = None context.n_predict = None @@ -27,7 +32,7 @@ def step_server_config(context, server_fqdn, server_port): context.user_api_key = None context.completions = [] - context.completion_threads = [] + context.concurrent_completion_tasks = [] context.prompts = [] @@ -61,39 +66,50 @@ def step_server_n_predict(context, n_predict): context.n_server_predict = int(n_predict) +@step(u'continuous batching') +def step_server_continuous_batching(context): + context.server_continuous_batching = True + + +@step(u"the server is starting") +def step_start_server(context): + start_server_background(context) + while True: + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + result = sock.connect_ex((context.server_fqdn, context.server_port)) + if result == 0: + return + + @step(u"the server is {expecting_status}") -def step_wait_for_the_server_to_be_started(context, expecting_status): +@async_run_until_complete +async def step_wait_for_the_server_to_be_started(context, expecting_status): match expecting_status: - case 'starting': - start_server_background(context) - server_started = False - while not server_started: - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: - result = sock.connect_ex((context.server_fqdn, context.server_port)) - if result == 0: - return 0 - case 'loading model': - wait_for_health_status(context, 503, 'loading model') case 'healthy': - wait_for_health_status(context, 200, 'ok') + await wait_for_health_status(context.base_url, 200, 'ok') + case 'ready' | 'idle': - wait_for_health_status(context, 200, 'ok', - params={'fail_on_no_slot': True}, - slots_idle=context.n_slots, - slots_processing=0) - request_slots_status(context, [{'id': slot_id, 'state': 0} for slot_id in range(context.n_slots)]) + await wait_for_health_status(context.base_url, 200, 'ok', + params={'fail_on_no_slot': 0, 'include_slots': 0}, + slots_idle=context.n_slots, + slots_processing=0, + expected_slots=[{'id': slot_id, 'state': 0} + for slot_id in range(context.n_slots)]) case 'busy': - wait_for_health_status(context, 503, 'no slot available', - params={'fail_on_no_slot': True}, - slots_idle=0, - slots_processing=context.n_slots) - request_slots_status(context, [{'id': slot_id, 'state': 1} for slot_id in range(context.n_slots)]) + await wait_for_health_status(context.base_url, 503, + 'no slot available', + params={'fail_on_no_slot': 0, 'include_slots': 0}, + slots_idle=0, + slots_processing=context.n_slots, + expected_slots=[{'id': slot_id, 'state': 1} + for slot_id in range(context.n_slots)]) case _: assert False, "unknown status" @step(u'all slots are {expected_slot_status_string}') -def step_all_slots_status(context, expected_slot_status_string): +@async_run_until_complete +async def step_all_slots_status(context, expected_slot_status_string): match expected_slot_status_string: case 'idle': expected_slot_status = 0 @@ -102,36 +118,40 @@ def step_all_slots_status(context, expected_slot_status_string): case _: assert False, "unknown status" - expected_slots = [] - for slot_id in range(context.n_slots): - expected_slots.append({ - 'id': slot_id, - 'state': expected_slot_status - }) - request_slots_status(context, expected_slots) + expected_slots = [{'id': slot_id, 'state': expected_slot_status} + for slot_id in range(context.n_slots)] + await request_slots_status(context, expected_slots) @step(u'a completion request with {api_error} api error') -def step_request_completion(context, api_error): - request_completion(context, context.prompts.pop(), - n_predict=context.n_predict, - expect_api_error=api_error == 'raised') - - -@step(u'{predicted_n} tokens are predicted with content: {content}') -def step_n_tokens_predicted_with_content(context, predicted_n, content): - assert_n_tokens_predicted(context.completions[0], int(predicted_n), content) +@async_run_until_complete +async def step_request_completion(context, api_error): + expect_api_error = api_error == 'raised' + completion = await request_completion(context.prompts.pop(), + context.base_url, + n_predict=context.n_predict, + server_seed=context.server_seed, + expect_api_error=expect_api_error, + user_api_key=context.user_api_key) + context.completions.append(completion) + print(f"Completion response: {completion}") + if expect_api_error: + assert completion == 401, f"completion must be an 401 status code: {completion}" + + +@step(u'{predicted_n} tokens are predicted matching {re_content}') +def step_n_tokens_predicted_with_content(context, predicted_n, re_content): + assert_n_tokens_predicted(context.completions.pop(), int(predicted_n), re_content) @step(u'{predicted_n} tokens are predicted') def step_n_tokens_predicted(context, predicted_n): - if int(predicted_n) > 0: - assert_n_tokens_predicted(context.completions[0], int(predicted_n)) + assert_n_tokens_predicted(context.completions.pop(), int(predicted_n)) @step(u'a user prompt {user_prompt}') def step_user_prompt(context, user_prompt): - context.user_prompt = user_prompt + context.prompts.append(user_prompt) @step(u'a system prompt {system_prompt}') @@ -151,7 +171,7 @@ def step_max_tokens(context, max_tokens): @step(u'streaming is {enable_streaming}') def step_streaming(context, enable_streaming): - context.enable_streaming = enable_streaming == 'enabled' or bool(enable_streaming) + context.enable_streaming = enable_streaming == 'enabled' @step(u'a user api key {user_api_key}') @@ -175,8 +195,35 @@ def step_server_api_key(context, server_api_key): @step(u'an OAI compatible chat completions request with {api_error} api error') -def step_oai_chat_completions(context, api_error): - oai_chat_completions(context, context.user_prompt, api_error=api_error == 'raised') +@async_run_until_complete +async def step_oai_chat_completions(context, api_error): + print(f"Submitting OAI compatible completions request...") + expect_api_error = api_error == 'raised' + completion = await oai_chat_completions(context.prompts.pop(), + context.system_prompt, + context.base_url, + False, + model=context.model if hasattr(context, 'model') else None, + + n_predict=context.n_predict + if hasattr(context, 'n_predict') else None, + + enable_streaming=context.enable_streaming + if hasattr(context, 'enable_streaming') else None, + + server_seed=context.server_seed + if hasattr(context, 'server_seed') else None, + + user_api_key=context.user_api_key + if hasattr(context, 'user_api_key') else None, + + expect_api_error=expect_api_error) + context.completions.append(completion) + print(f"Completion response: {completion}") + if expect_api_error: + assert completion == 401, f"completion must be an 401 status code: {completion}" + + print(f"Completion response: {completion}") @step(u'a prompt') @@ -190,22 +237,49 @@ def step_a_prompt_prompt(context, prompt): @step(u'concurrent completion requests') -def step_concurrent_completion_requests(context): - concurrent_requests(context, request_completion) +@async_run_until_complete() +async def step_concurrent_completion_requests(context): + await concurrent_completion_requests(context, + request_completion, + # prompt is inserted automatically + context.base_url, + n_predict=context.n_predict if hasattr(context, 'n_predict') else None, + server_seed=context.server_seed if hasattr(context, 'server_seed') else None, + user_api_key=context.user_api_key if hasattr(context, + 'user_api_key') else None) @step(u'concurrent OAI completions requests') -def step_oai_chat_completions(context): - concurrent_requests(context, oai_chat_completions) - - -@step(u'all prompts are predicted') -def step_all_prompts_are_predicted(context): - for completion_thread in context.completion_threads: - completion_thread.join() - assert len(context.completions) == len(context.completion_threads) - for completion in context.completions: - assert_n_tokens_predicted(completion) +@async_run_until_complete +async def step_oai_chat_completions(context): + await concurrent_completion_requests(context, oai_chat_completions, + # user_prompt is inserted automatically + context.system_prompt, + context.base_url, + True, # async_client + model=context.model + if hasattr(context, 'model') else None, + n_predict=context.n_predict + if hasattr(context, 'n_predict') else None, + enable_streaming=context.enable_streaming + if hasattr(context, 'enable_streaming') else None, + server_seed=context.server_seed + if hasattr(context, 'server_seed') else None, + user_api_key=context.user_api_key + if hasattr(context, 'user_api_key') else None) + + +@step(u'all prompts are predicted with {n_predict} tokens') +@async_run_until_complete +async def step_all_prompts_are_predicted(context, n_predict): + n_completion_tasks = len(context.concurrent_completion_tasks) + print(f"Waiting for all {n_completion_tasks} completion responses...") + for task_no in range(n_completion_tasks): + context.completions.append(await context.concurrent_completion_tasks.pop()) + n_completions = len(context.completions) + assert n_completions > 0 + for i in range(n_completions): + assert_n_tokens_predicted(context.completions.pop(), expected_predicted_n=int(n_predict)) @step(u'embeddings are computed for') @@ -269,126 +343,228 @@ def step_check_options_header_value(context, cors_header, cors_header_value): assert context.options_response.headers[cors_header] == cors_header_value -def concurrent_requests(context, f_completion, *argv): - context.completions.clear() - context.completion_threads.clear() - for prompt in context.prompts: - completion_thread = threading.Thread(target=f_completion, args=(context, prompt, *argv)) - completion_thread.start() - context.completion_threads.append(completion_thread) - context.prompts.clear() +async def concurrent_completion_requests(context, f_completion, *args, **kwargs): + n_prompts = len(context.prompts) + print(f"starting {n_prompts} concurrent completion requests...") + assert n_prompts > 0 + for prompt_no in range(n_prompts): + shifted_args = [context.prompts.pop(), *args] + context.concurrent_completion_tasks.append(asyncio.create_task(f_completion(*shifted_args, **kwargs))) + await asyncio.sleep(0.1) -def request_completion(context, prompt, n_predict=None, expect_api_error=None): +async def request_completion(prompt, + base_url, + n_predict=None, + server_seed=None, + expect_api_error=None, + user_api_key=None): + print(f"Sending completion request: {prompt}") origin = "my.super.domain" headers = { 'Origin': origin } - if context.user_api_key is not None: - print(f"Set user_api_key: {context.user_api_key}") - headers['Authorization'] = f'Bearer {context.user_api_key}' - - response = requests.post(f'{context.base_url}/completion', - json={ - "prompt": prompt, - "n_predict": int(n_predict) if n_predict is not None else context.n_predict, - "seed": context.server_seed if context.server_seed is not None else 42 - }, - headers=headers) - if expect_api_error is not None and not expect_api_error: - assert response.status_code == 200 - assert response.headers['Access-Control-Allow-Origin'] == origin - context.completions.append(response.json()) - else: - assert response.status_code == 401 - - -def oai_chat_completions(context, user_prompt, api_error=None): - openai.api_key = 'nope' # openai client always expects an api_keu - if context.user_api_key is not None: - openai.api_key = context.user_api_key - openai.api_base = f'{context.base_url}/v1/chat' - try: - chat_completion = openai.Completion.create( - messages=[ - { - "role": "system", - "content": context.system_prompt, - }, - { - "role": "user", - "content": user_prompt, - } - ], - model=context.model, - max_tokens=context.n_predict, - stream=context.enable_streaming, - seed=context.server_seed if context.server_seed is not None else 42 - ) - except openai.error.APIError: - if api_error is not None and api_error: - return - if context.enable_streaming: - completion_response = { - 'content': '', - 'timings': { - 'predicted_n': 0 + if user_api_key is not None: + print(f"Set user_api_key: {user_api_key}") + headers['Authorization'] = f'Bearer {user_api_key}' + + async with aiohttp.ClientSession() as session: + async with session.post(f'{base_url}/completion', + json={ + "prompt": prompt, + "n_predict": int(n_predict) if n_predict is not None else -1, + "seed": server_seed if server_seed is not None else 42 + }, + headers=headers) as response: + if expect_api_error is None or not expect_api_error: + assert response.status == 200 + assert response.headers['Access-Control-Allow-Origin'] == origin + return await response.json() + else: + return response.status + + +async def oai_chat_completions(user_prompt, + system_prompt, + base_url, + async_client, + model=None, + n_predict=None, + enable_streaming=None, + server_seed=None, + user_api_key=None, + expect_api_error=None): + print(f"Sending OAI Chat completions request: {user_prompt}") + # openai client always expects an api key + user_api_key = user_api_key if user_api_key is not None else 'nope' + seed = server_seed if server_seed is not None else 42 + enable_streaming = enable_streaming if enable_streaming is not None else False + payload = { + "messages": [ + { + "role": "system", + "content": system_prompt, + }, + { + "role": "user", + "content": user_prompt, } + ], + "model": model, + "max_tokens": n_predict, + "stream": enable_streaming, + "seed": seed + } + completion_response = { + 'content': '', + 'timings': { + 'predicted_n': 0 } - for chunk in chat_completion: - assert len(chunk.choices) == 1 - delta = chunk.choices[0].delta - if 'content' in delta: - completion_response['content'] += delta['content'] - completion_response['timings']['predicted_n'] += 1 - context.completions.append(completion_response) + } + if async_client: + origin = 'llama.cpp' + headers = {'Authorization': f'Bearer {user_api_key}', 'Origin': origin} + async with aiohttp.ClientSession() as session: + async with session.post(f'{base_url}/v1/chat/completions', + json=payload, + headers=headers) as response: + if enable_streaming: + print("payload", payload) + assert response.status == 200 + assert response.headers['Access-Control-Allow-Origin'] == origin + assert response.headers['Content-Type'] == "text/event-stream" + + async for line_in_bytes in response.content: + line = line_in_bytes.decode('utf8') + event_data = line.split(': ', 1) + assert event_data[0] == 'data', f'{event_data}' + chunk_raw = event_data[1] + + chunk = json.loads(chunk_raw) + assert len(chunk['choices']) == 1 + delta = chunk['choices'][0]['delta'] + if 'content' in delta: + completion_response['content'] += delta['content'] + completion_response['timings']['predicted_n'] += 1 + print(f"XXXXXXXXXXXXXXXXXcompletion_response: {completion_response}") + else: + print(f"raw completion response: {response}") + if expect_api_error is None or not expect_api_error: + assert response.status == 200 + assert response.headers['Access-Control-Allow-Origin'] == origin + assert response.headers['Content-Type'] == "application/json; charset=utf-8" + chat_completion_raw = await response.json() + completion_response = { + 'content': chat_completion_raw['choices'][0]['message'], + 'timings': { + 'predicted_n': chat_completion_raw['usage']['completion_tokens'] + } + } + else: + return response.status else: - assert len(chat_completion.choices) == 1 - context.completions.append({ - 'content': chat_completion.choices[0].message, - 'timings': { - 'predicted_n': chat_completion.usage.completion_tokens + try: + openai.api_key = user_api_key + openai.api_base = f'{base_url}/v1/chat' + chat_completion = openai.Completion.create( + messages=payload['messages'], + model=model, + max_tokens=n_predict, + stream=enable_streaming, + seed=seed + ) + except openai.error.APIError as e: + if expect_api_error is not None and expect_api_error: + return 401 + else: + assert False, f'error raised: {e}' + + if enable_streaming: + for chunk in chat_completion: + assert len(chunk.choices) == 1 + delta = chunk.choices[0].delta + if 'content' in delta: + completion_response['content'] += delta['content'] + completion_response['timings']['predicted_n'] += 1 + else: + assert len(chat_completion.choices) == 1 + completion_response = { + 'content': chat_completion.choices[0].message.content, + 'timings': { + 'predicted_n': chat_completion.usage.completion_tokens + } } - }) + print("OAI response formatted to llama.cpp", completion_response) + return completion_response -def assert_n_tokens_predicted(completion_response, expected_predicted_n=None, expected_content=None): +def assert_n_tokens_predicted(completion_response, expected_predicted_n=None, re_content=None): content = completion_response['content'] n_predicted = completion_response['timings']['predicted_n'] assert len(content) > 0, "no token predicted" if expected_predicted_n is not None: assert n_predicted == expected_predicted_n, (f'invalid number of tokens predicted:' f' {n_predicted} <> {expected_predicted_n}') - if expected_content is not None: - expected_content = expected_content.replace('', ' ').replace('', '\n') - assert content == expected_content, (f'invalid tokens predicted:' - f' ```\n{content}\n``` <> ```\n{expected_content}\n```') - - -def wait_for_health_status(context, expected_http_status_code, - expected_health_status, - params=None, - slots_idle=None, - slots_processing=None): - while True: - health_response = requests.get(f'{context.base_url}/health', params) - status_code = health_response.status_code - health = health_response.json() - if (status_code == expected_http_status_code - and health['status'] == expected_health_status - and (slots_idle is None or health['slots_idle'] == slots_idle) - and (slots_processing is None or health['slots_processing'] == slots_processing)): - break - - -def request_slots_status(context, expected_slots): - slots_response = requests.get(f'{context.base_url}/slots') - assert slots_response.status_code == 200 - slots = slots_response.json() + if re_content is not None: + re_content = '^.*' + re_content.replace('', '|') + '.*$' + assert re.match(re_content, content, flags=RegexFlag.IGNORECASE | RegexFlag.MULTILINE | RegexFlag.DOTALL), ( + f'invalid tokens predicted:' + f' ```\n{content}\n``` do not match /{re_content}/') + + +async def wait_for_health_status(base_url, + expected_http_status_code, + expected_health_status, + params=None, + slots_idle=None, + slots_processing=None, + expected_slots=None): + print(f"Starting checking for health for expected_health_status={expected_health_status}") + timeout = 3 # seconds + interval = 0.5 + counter = 0 + async with aiohttp.ClientSession() as session: + while True: + async with await session.get(f'{base_url}/health', params=params) as health_response: + status_code = health_response.status + health = await health_response.json() + print(f"HEALTH - response for expected health status='{expected_health_status}' on " + f"'{base_url}/health'?{params} is {health}") + if (status_code == expected_http_status_code + and health['status'] == expected_health_status + and (slots_idle is None or health['slots_idle'] == slots_idle) + and (slots_processing is None or health['slots_processing'] == slots_processing)): + if expected_slots is not None: + assert_slots_status(health['slots'], expected_slots) + return + if (status_code == expected_http_status_code + and health['status'] == expected_health_status + and (slots_idle is None or health['slots_idle'] == slots_idle) + and (slots_processing is None or health['slots_processing'] == slots_processing)): + if expected_slots is not None: + assert_slots_status(health['slots'], expected_slots) + return + await asyncio.sleep(interval) + counter += interval + if counter >= timeout: + assert False, 'timeout exceeded' + + +async def request_slots_status(context, expected_slots): + async with aiohttp.ClientSession() as session: + async with await session.get(f'{context.base_url}/slots') as slots_response: + assert slots_response.status == 200 + slots = await slots_response.json() + assert_slots_status(slots, expected_slots) + + +def assert_slots_status(slots, expected_slots): assert len(slots) == len(expected_slots) - for expected, slot in zip(expected_slots, slots): + for slot_id, (expected, slot) in enumerate(zip(expected_slots, slots)): for key in expected: - assert expected[key] == slot[key], f"expected[{key}] != slot[{key}]" + assert expected[key] == slot[key], (f"invalid slot {slot_id}" + f" expected[{key}] != slot[{key}]" + f" = {expected[key]} != {slot[key]}") def start_server_background(context): @@ -398,6 +574,8 @@ def start_server_background(context): server_args = [ '--model', context.model_file ] + if context.server_continuous_batching: + server_args.append('--cont-batching') if context.model_alias is not None: server_args.extend(['--alias', context.model_alias]) if context.server_seed is not None: diff --git a/examples/server/tests/requirements.txt b/examples/server/tests/requirements.txt index b64fbc6ba0c21..3e51b12dc8207 100644 --- a/examples/server/tests/requirements.txt +++ b/examples/server/tests/requirements.txt @@ -1,2 +1,3 @@ +aiohttp~=3.9.3 behave~=1.2.6 openai~=0.25.0 diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index 7d43ddfc8cd68..3b101ad3d97e2 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -3,4 +3,4 @@ set -eu # Start @llama.cpp scenario -behave --summary --stop --tags llama.cpp +behave --summary --stop --no-capture --tags llama.cpp From 597c181abbf05bcadf55dbc4da06d51cd1eca4f4 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Thu, 22 Feb 2024 21:58:28 +0100 Subject: [PATCH 38/98] server: tests: ci do not take a model anymore, fix trigger patch --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 036543806c5c5..ae75e50bba6c4 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -50,4 +50,4 @@ jobs: id: server_integration_test run: | cd examples/server/tests - ./tests.sh ../../../stories260K.gguf + ./tests.sh From f820e10fa74e68cd7a4ffef6329d07d71d291f92 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Thu, 22 Feb 2024 23:18:42 +0100 Subject: [PATCH 39/98] server: tests: ci ensure the server is stopped before scenario, and do not quit while the server is listening --- examples/server/tests/features/environment.py | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index cae6ea8bda353..e84acfe77ce18 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -1,8 +1,38 @@ +import multiprocessing +import os +import socket +import subprocess import time +from contextlib import closing +from signal import SIGKILL + + +def before_scenario(context, scenario): + if is_server_listening("localhost", 8080): + assert False, "Server already started" def after_scenario(context, scenario): - print("stopping server...") + print(f"stopping server pid={context.server_process.pid} ...") context.server_process.kill() - # Wait few for socket to be free up - time.sleep(0.05) + # Wait few for socket to free up + time.sleep(0.1) + + attempts = 0 + while is_server_listening(context.server_fqdn, context.server_port): + print(f"stopping server pid={context.server_process.pid} ...") + os.kill(context.server_process.pid, SIGKILL) + time.sleep(0.5) + attempts += 1 + if attempts > 1: + print(f"Server dandling exits, killing all {context.server_path} ...") + process = subprocess.run(['killall', '-9', context.server_path], + stderr=subprocess.PIPE, + universal_newlines=True) + print(process) + + +def is_server_listening(server_fqdn, server_port): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + result = sock.connect_ex((server_fqdn, server_port)) + return result == 0 From aa591ef12d04568a22316c1ceca7b711bb8900b6 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Thu, 22 Feb 2024 23:37:56 +0100 Subject: [PATCH 40/98] server: tests: add Multi users with total number of tokens to predict exceeds the KV Cache size --- .../server/tests/features/parallel.feature | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/examples/server/tests/features/parallel.feature b/examples/server/tests/features/parallel.feature index b420a8501d710..8fe1befd05a7d 100644 --- a/examples/server/tests/features/parallel.feature +++ b/examples/server/tests/features/parallel.feature @@ -52,3 +52,33 @@ Feature: Parallel | streaming | n_predict | | disabled | 512 | #| enabled | 512 | FIXME: phymbert: need to investigate why in aiohttp with streaming only one token is generated + + Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size #3969 + Given a server listening on localhost:8080 + And a model file stories260K.gguf + And 42 as server seed + And 2 slots + And 1024 KV cache size + Then the server is starting + Then the server is healthy + Given a prompt: + """ + Write a very long story about AI. + """ + And a prompt: + """ + Write another very long music lyrics. + """ + And a prompt: + """ + Write a very long poem. + """ + And a prompt: + """ + Write a very long joke. + """ + And 2048 max tokens to predict + Given concurrent completion requests + Then the server is busy + Then the server is idle + Then all prompts are predicted From 26b66c54964a2c1c03ecb77b5a9d0f7e53bb1b55 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Thu, 22 Feb 2024 23:38:47 +0100 Subject: [PATCH 41/98] server: tests: Fix some random behavior where the wait for busy status is missing --- examples/server/tests/features/steps/steps.py | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 1e27ce274d2f4..71327728ae3df 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -86,17 +86,17 @@ def step_start_server(context): async def step_wait_for_the_server_to_be_started(context, expecting_status): match expecting_status: case 'healthy': - await wait_for_health_status(context.base_url, 200, 'ok') + await wait_for_health_status(context, context.base_url, 200, 'ok') case 'ready' | 'idle': - await wait_for_health_status(context.base_url, 200, 'ok', + await wait_for_health_status(context, context.base_url, 200, 'ok', params={'fail_on_no_slot': 0, 'include_slots': 0}, slots_idle=context.n_slots, slots_processing=0, expected_slots=[{'id': slot_id, 'state': 0} for slot_id in range(context.n_slots)]) case 'busy': - await wait_for_health_status(context.base_url, 503, + await wait_for_health_status(context, context.base_url, 503, 'no slot available', params={'fail_on_no_slot': 0, 'include_slots': 0}, slots_idle=0, @@ -269,17 +269,24 @@ async def step_oai_chat_completions(context): if hasattr(context, 'user_api_key') else None) +@async_run_until_complete +@step(u'all prompts are predicted') +async def step_impl(context): + await all_prompts_are_predicted(context) + + @step(u'all prompts are predicted with {n_predict} tokens') @async_run_until_complete async def step_all_prompts_are_predicted(context, n_predict): - n_completion_tasks = len(context.concurrent_completion_tasks) - print(f"Waiting for all {n_completion_tasks} completion responses...") - for task_no in range(n_completion_tasks): - context.completions.append(await context.concurrent_completion_tasks.pop()) - n_completions = len(context.completions) + expected_predicted_n = int(n_predict) + await all_prompts_are_predicted(context, expected_predicted_n) + + +async def all_prompts_are_predicted(context, expected_predicted_n): + n_completions = await gather_concurrent_completions_tasks(context) assert n_completions > 0 for i in range(n_completions): - assert_n_tokens_predicted(context.completions.pop(), expected_predicted_n=int(n_predict)) + assert_n_tokens_predicted(context.completions.pop(), expected_predicted_n=expected_predicted_n) @step(u'embeddings are computed for') @@ -448,7 +455,6 @@ async def oai_chat_completions(user_prompt, completion_response['timings']['predicted_n'] += 1 print(f"XXXXXXXXXXXXXXXXXcompletion_response: {completion_response}") else: - print(f"raw completion response: {response}") if expect_api_error is None or not expect_api_error: assert response.status == 200 assert response.headers['Access-Control-Allow-Origin'] == origin @@ -512,7 +518,17 @@ def assert_n_tokens_predicted(completion_response, expected_predicted_n=None, re f' ```\n{content}\n``` do not match /{re_content}/') -async def wait_for_health_status(base_url, +async def gather_concurrent_completions_tasks(context): + n_completion_tasks = len(context.concurrent_completion_tasks) + print(f"Waiting for all {n_completion_tasks} completion responses...") + for task_no in range(n_completion_tasks): + context.completions.append(await context.concurrent_completion_tasks.pop()) + n_completions = len(context.completions) + return n_completions + + +async def wait_for_health_status(context, + base_url, expected_http_status_code, expected_health_status, params=None, @@ -545,8 +561,17 @@ async def wait_for_health_status(base_url, assert_slots_status(health['slots'], expected_slots) return await asyncio.sleep(interval) + counter += interval if counter >= timeout: + # Sometimes health requests are triggered after completions are predicted + if expected_http_status_code == 503: + if len(context.completions) == 0: + print("\x1b[5;37;43mWARNING: forcing concurrent completions task, busy health check missed") + n_completions = await gather_concurrent_completions_tasks(context) + if n_completions > 0: + return + assert False, 'timeout exceeded' @@ -572,6 +597,8 @@ def start_server_background(context): if 'LLAMA_SERVER_BIN_PATH' in os.environ: context.server_path = os.environ['LLAMA_SERVER_BIN_PATH'] server_args = [ + '--host', context.server_fqdn, + '--port', context.server_port, '--model', context.model_file ] if context.server_continuous_batching: From 51f527440a066935b94539af1592ce2e3e92c5c5 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 00:37:42 +0100 Subject: [PATCH 42/98] server: tests: ci triggered on any changes on server example path --- .github/workflows/server-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index ae75e50bba6c4..a4242ea12571e 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -7,10 +7,10 @@ on: branches: - master - test/server-add-ci-test # FIXME remove - paths: ['.github/workflows/**', '**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m'] + paths: ['.github/workflows/**', '**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m', 'examples/server/**.*'] pull_request: types: [opened, synchronize, reopened] - paths: ['**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m'] + paths: ['**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m', 'examples/server/**.*'] jobs: ubuntu-latest-cmake: From cba6d4ea179420c688d656c3e367f977f52ffe4a Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 00:54:44 +0100 Subject: [PATCH 43/98] server: tests: minor fix missing param. --- examples/server/tests/features/steps/steps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 71327728ae3df..3bfbd6824550a 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -282,7 +282,7 @@ async def step_all_prompts_are_predicted(context, n_predict): await all_prompts_are_predicted(context, expected_predicted_n) -async def all_prompts_are_predicted(context, expected_predicted_n): +async def all_prompts_are_predicted(context, expected_predicted_n=None): n_completions = await gather_concurrent_completions_tasks(context) assert n_completions > 0 for i in range(n_completions): @@ -567,7 +567,8 @@ async def wait_for_health_status(context, # Sometimes health requests are triggered after completions are predicted if expected_http_status_code == 503: if len(context.completions) == 0: - print("\x1b[5;37;43mWARNING: forcing concurrent completions task, busy health check missed") + print("\x1b[5;37;43mWARNING: forcing concurrents completions tasks," + " busy health check missed\x1b[0m") n_completions = await gather_concurrent_completions_tasks(context) if n_completions > 0: return From 1bd07e56c491cb0921ab156c4b3ad8043871d274 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 01:25:08 +0100 Subject: [PATCH 44/98] server: tests: assert embeddings are actually computed, make the embeddings endpoint configurable. Add logs to investigate why the CI server test job is not starting --- examples/server/tests/features/server.feature | 1 + examples/server/tests/features/steps/steps.py | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index c36b42e07d7f7..a98d92c09ab45 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -8,6 +8,7 @@ Feature: llama.cpp server And 42 as server seed And 32 KV cache size And 1 slots + And embeddings extraction And 32 server max tokens to predict Then the server is starting Then the server is healthy diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 3bfbd6824550a..ac1bbd6bebb65 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -4,6 +4,7 @@ import re import socket import subprocess +import time from contextlib import closing from re import RegexFlag @@ -21,13 +22,14 @@ def step_server_config(context, server_fqdn, server_port): context.base_url = f'http://{context.server_fqdn}:{context.server_port}' - context.server_continuous_batching = False context.model_alias = None context.n_ctx = None context.n_predict = None context.n_server_predict = None context.n_slots = None context.server_api_key = None + context.server_continuous_batching = False + context.server_embeddings = False context.server_seed = None context.user_api_key = None @@ -70,15 +72,26 @@ def step_server_n_predict(context, n_predict): def step_server_continuous_batching(context): context.server_continuous_batching = True +@step(u'embeddings extraction') +def step_server_embeddings(context): + context.server_embeddings = True + @step(u"the server is starting") def step_start_server(context): start_server_background(context) + attempts = 0 while True: with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: result = sock.connect_ex((context.server_fqdn, context.server_port)) if result == 0: + print("server started!") return + attempts += 1 + if attempts > 20: + assert False, "server not started" + print("waiting for server to start...") + time.sleep(0.1) @step(u"the server is {expecting_status}") @@ -301,6 +314,11 @@ def step_compute_embedding(context): @step(u'embeddings are generated') def step_compute_embeddings(context): assert len(context.embeddings) > 0 + embeddings_computed = False + for emb in context.embeddings: + if emb != 0: + embeddings_computed = True + assert embeddings_computed, f"Embeddings: {context.embeddings}" @step(u'an OAI compatible embeddings computation request for') @@ -436,7 +454,8 @@ async def oai_chat_completions(user_prompt, json=payload, headers=headers) as response: if enable_streaming: - print("payload", payload) + # FIXME: does not work; the server is generating only one token + print("DEBUG payload", payload) assert response.status == 200 assert response.headers['Access-Control-Allow-Origin'] == origin assert response.headers['Content-Type'] == "text/event-stream" @@ -453,7 +472,7 @@ async def oai_chat_completions(user_prompt, if 'content' in delta: completion_response['content'] += delta['content'] completion_response['timings']['predicted_n'] += 1 - print(f"XXXXXXXXXXXXXXXXXcompletion_response: {completion_response}") + print(f"DEBUG completion_response: {completion_response}") else: if expect_api_error is None or not expect_api_error: assert response.status == 200 @@ -500,7 +519,7 @@ async def oai_chat_completions(user_prompt, 'predicted_n': chat_completion.usage.completion_tokens } } - print("OAI response formatted to llama.cpp", completion_response) + print("OAI response formatted to llama.cpp:", completion_response) return completion_response @@ -567,7 +586,7 @@ async def wait_for_health_status(context, # Sometimes health requests are triggered after completions are predicted if expected_http_status_code == 503: if len(context.completions) == 0: - print("\x1b[5;37;43mWARNING: forcing concurrents completions tasks," + print("\x1b[33;42mWARNING: forcing concurrents completions tasks," " busy health check missed\x1b[0m") n_completions = await gather_concurrent_completions_tasks(context) if n_completions > 0: @@ -604,6 +623,8 @@ def start_server_background(context): ] if context.server_continuous_batching: server_args.append('--cont-batching') + if context.server_embeddings: + server_args.append('--embedding') if context.model_alias is not None: server_args.extend(['--alias', context.model_alias]) if context.server_seed is not None: @@ -620,3 +641,4 @@ def start_server_background(context): context.server_process = subprocess.Popen( [str(arg) for arg in [context.server_path, *server_args]], close_fds=True) + print(f"server pid={context.server_process.pid}") From 14b6ede152a378d50f721d8803b73d554446c512 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 01:29:39 +0100 Subject: [PATCH 45/98] server: tests: minor color change --- examples/server/tests/features/steps/steps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index ac1bbd6bebb65..86e039ff2e582 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -85,7 +85,7 @@ def step_start_server(context): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: result = sock.connect_ex((context.server_fqdn, context.server_port)) if result == 0: - print("server started!") + print("\x1b[33;42mserver started!\x1b[0m") return attempts += 1 if attempts > 20: @@ -586,7 +586,7 @@ async def wait_for_health_status(context, # Sometimes health requests are triggered after completions are predicted if expected_http_status_code == 503: if len(context.completions) == 0: - print("\x1b[33;42mWARNING: forcing concurrents completions tasks," + print("\x1b[5;37;43WARNING: forcing concurrents completions tasks," " busy health check missed\x1b[0m") n_completions = await gather_concurrent_completions_tasks(context) if n_completions > 0: From b38b9e60a1693136c601f47aceba3807bcd9c87a Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 01:31:56 +0100 Subject: [PATCH 46/98] server: tests: minor fix server --alias param passed twice --- examples/server/tests/features/steps/steps.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 86e039ff2e582..d6e6cd48a3ef9 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -627,8 +627,6 @@ def start_server_background(context): server_args.append('--embedding') if context.model_alias is not None: server_args.extend(['--alias', context.model_alias]) - if context.server_seed is not None: - server_args.extend(['--alias', context.model_alias]) if context.n_ctx is not None: server_args.extend(['--ctx-size', context.n_ctx]) if context.n_slots is not None: From 70e90558ae0eb59021a5f79163bd3336ee8e0c4b Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 01:46:08 +0100 Subject: [PATCH 47/98] server: tests: add log in server start to identify why the server does not listen on the CI --- examples/server/tests/features/steps/steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index d6e6cd48a3ef9..adb7a0119d604 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -90,7 +90,7 @@ def step_start_server(context): attempts += 1 if attempts > 20: assert False, "server not started" - print("waiting for server to start...") + print(f"waiting for server to start, connect error code = {result}...") time.sleep(0.1) From 2f756f84dfc2d931766bad812745ae18282176fb Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 01:59:29 +0100 Subject: [PATCH 48/98] server: tests: allow to override the server port before launching tests --- .github/workflows/server-test.yml | 2 +- examples/server/tests/README.md | 4 ++++ examples/server/tests/features/environment.py | 5 ++++- examples/server/tests/features/steps/steps.py | 3 +++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index a4242ea12571e..7c56ebb8de0c6 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -50,4 +50,4 @@ jobs: id: server_integration_test run: | cd examples/server/tests - ./tests.sh + PORT=8888 ./tests.sh diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 5e5da6ff8fa10..1892bafc929f0 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -11,6 +11,10 @@ Server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_ 1. `../../../scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf` 3. Start the test: `./tests.sh` +It's possible to override some scenario steps values with environment variables: + - `$PORT` -> `context.server_port` to set the listening port of the server during scenario, default: `8080` + - `$LLAMA_SERVER_BIN_PATH` -> to change the server binary path, default: `../../../build/bin/server` + To change the server path, use `LLAMA_SERVER_BIN_PATH` environment variable. ### Skipped scenario diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index e84acfe77ce18..652b4f30c0335 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -8,7 +8,10 @@ def before_scenario(context, scenario): - if is_server_listening("localhost", 8080): + port = 8080 + if 'PORT' in os.environ: + port = int(os.environ['PORT']) + if is_server_listening("localhost", port): assert False, "Server already started" diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index adb7a0119d604..a5a516252c6bd 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -19,6 +19,9 @@ def step_server_config(context, server_fqdn, server_port): context.server_fqdn = server_fqdn context.server_port = int(server_port) + if 'PORT' in os.environ: + context.server_port = int(os.environ['PORT']) + print(f"$PORT set, overriding server port with to {context.server_port}") context.base_url = f'http://{context.server_fqdn}:{context.server_port}' From 6a215e5359cb71f57a7e5a2a5f8c2f0b8f5bf10d Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:06:36 +0100 Subject: [PATCH 49/98] server: tests: ci adding container to specify server port and allow the server to listen to --- .github/workflows/server-test.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 7c56ebb8de0c6..b52a6337127b9 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -15,6 +15,10 @@ on: jobs: ubuntu-latest-cmake: runs-on: ubuntu-latest + container: + image: ubuntu:latest + ports: + - 8888 steps: - name: Clone @@ -24,8 +28,8 @@ jobs: - name: Dependencies id: depends run: | - sudo apt-get update - sudo apt-get install build-essential + apt-get update + apt-get -y install build-essential - name: Build id: cmake_build From 2bb4732c01bc6d5e46aa576fac05d274a84b7e8e Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:13:30 +0100 Subject: [PATCH 50/98] server: tests: ci adding cmake as it is not present by default in ubuntu base --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index b52a6337127b9..a51184e3365e7 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -29,7 +29,7 @@ jobs: id: depends run: | apt-get update - apt-get -y install build-essential + apt-get -y install build-essential cmake - name: Build id: cmake_build From d0e00508434e48d36a0cfdcaef7de475238b12fc Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:16:56 +0100 Subject: [PATCH 51/98] server: tests: ci adding python3-pip as it is not present by default in ubuntu base --- .github/workflows/server-test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index a51184e3365e7..e7e9dff0f0db4 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -29,7 +29,10 @@ jobs: id: depends run: | apt-get update - apt-get -y install build-essential cmake + apt-get -y install \ + build-essential \ + cmake \ + python3-pip - name: Build id: cmake_build From 6e71126c129a95360408ba8e3a3b6daf611998d2 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:19:47 +0100 Subject: [PATCH 52/98] server: tests: ci adding curl as it is not present by default in ubuntu base for the hf.sh script --- .github/workflows/server-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index e7e9dff0f0db4..589cbf956c9ce 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -32,7 +32,8 @@ jobs: apt-get -y install \ build-essential \ cmake \ - python3-pip + python3-pip \ + curl - name: Build id: cmake_build From 6bba3be151a425e736d9b9f3b5b15c36e92ff6cc Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:31:30 +0100 Subject: [PATCH 53/98] server: tests: ci adding psmisc as it is not present by default in ubuntu base killall --- .github/workflows/server-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 589cbf956c9ce..b7a20bbe5e222 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -33,7 +33,8 @@ jobs: build-essential \ cmake \ python3-pip \ - curl + curl \ + psmisc - name: Build id: cmake_build From 5110de08e384206a6d6920255e323e17ee4e85ca Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:31:44 +0100 Subject: [PATCH 54/98] server: tests: fix coloring console --- examples/server/tests/features/steps/steps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index a5a516252c6bd..7375fcba92a5c 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -589,8 +589,8 @@ async def wait_for_health_status(context, # Sometimes health requests are triggered after completions are predicted if expected_http_status_code == 503: if len(context.completions) == 0: - print("\x1b[5;37;43WARNING: forcing concurrents completions tasks," - " busy health check missed\x1b[0m") + print("\x1b[5;37;43mWARNING: forcing concurrents completions tasks," + " busy health check missed, probably too fast inference\x1b[0m") n_completions = await gather_concurrent_completions_tasks(context) if n_completions > 0: return From bedf37c9d180b3c9203ce3506efaa19c5978c4b3 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:38:37 +0100 Subject: [PATCH 55/98] server: tests: reducing n_ctx and n_predict for // prompts as it is too slow in the CI. --- examples/server/tests/features/parallel.feature | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/server/tests/features/parallel.feature b/examples/server/tests/features/parallel.feature index 8fe1befd05a7d..d4d403eade843 100644 --- a/examples/server/tests/features/parallel.feature +++ b/examples/server/tests/features/parallel.feature @@ -6,7 +6,7 @@ Feature: Parallel And a model file stories260K.gguf And a model alias tinyllama-2 And 42 as server seed - And 32 KV cache size + And 64 KV cache size And 2 slots And continuous batching Then the server is starting @@ -29,7 +29,7 @@ Feature: Parallel Then all prompts are predicted with tokens Examples: | n_predict | - | 512 | + | 128 | Scenario Outline: Multi users OAI completions compatibility Given a system prompt You are a writer. @@ -50,15 +50,15 @@ Feature: Parallel Then all prompts are predicted with tokens Examples: | streaming | n_predict | - | disabled | 512 | - #| enabled | 512 | FIXME: phymbert: need to investigate why in aiohttp with streaming only one token is generated + | disabled | 64 | + #| enabled | 64 | FIXME: phymbert: need to investigate why in aiohttp with streaming only one token is generated Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size #3969 Given a server listening on localhost:8080 And a model file stories260K.gguf And 42 as server seed And 2 slots - And 1024 KV cache size + And 64 KV cache size Then the server is starting Then the server is healthy Given a prompt: @@ -77,7 +77,7 @@ Feature: Parallel """ Write a very long joke. """ - And 2048 max tokens to predict + And 128 max tokens to predict Given concurrent completion requests Then the server is busy Then the server is idle From 530d3ae4c40baedbc73b0f62abb9e988f54f0fb2 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 02:38:54 +0100 Subject: [PATCH 56/98] server: tests: reducing sleep time during scenario --- examples/server/tests/features/environment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index 652b4f30c0335..01877cd10d486 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -19,15 +19,15 @@ def after_scenario(context, scenario): print(f"stopping server pid={context.server_process.pid} ...") context.server_process.kill() # Wait few for socket to free up - time.sleep(0.1) + time.sleep(0.05) attempts = 0 while is_server_listening(context.server_fqdn, context.server_port): print(f"stopping server pid={context.server_process.pid} ...") os.kill(context.server_process.pid, SIGKILL) - time.sleep(0.5) + time.sleep(0.1) attempts += 1 - if attempts > 1: + if attempts > 5: print(f"Server dandling exits, killing all {context.server_path} ...") process = subprocess.run(['killall', '-9', context.server_path], stderr=subprocess.PIPE, From 36ddb962d874509dd2aa5fbba20edcd71578e689 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:09:19 +0100 Subject: [PATCH 57/98] server: tests: parallel fix server is started twice, add colors to help to monitor in the CI jobs --- examples/server/tests/features/environment.py | 1 + examples/server/tests/features/parallel.feature | 9 +-------- examples/server/tests/features/steps/steps.py | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index 01877cd10d486..de2f9c9828d02 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -8,6 +8,7 @@ def before_scenario(context, scenario): + print(f"\x1b[33;42mStarting new scenario: {scenario.name}!\x1b[0m") port = 8080 if 'PORT' in os.environ: port = int(os.environ['PORT']) diff --git a/examples/server/tests/features/parallel.feature b/examples/server/tests/features/parallel.feature index d4d403eade843..07be39ef58a4b 100644 --- a/examples/server/tests/features/parallel.feature +++ b/examples/server/tests/features/parallel.feature @@ -50,17 +50,10 @@ Feature: Parallel Then all prompts are predicted with tokens Examples: | streaming | n_predict | - | disabled | 64 | + | disabled | 128 | #| enabled | 64 | FIXME: phymbert: need to investigate why in aiohttp with streaming only one token is generated Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size #3969 - Given a server listening on localhost:8080 - And a model file stories260K.gguf - And 42 as server seed - And 2 slots - And 64 KV cache size - Then the server is starting - Then the server is healthy Given a prompt: """ Write a very long story about AI. diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 7375fcba92a5c..cfe03e799c2e7 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -88,7 +88,7 @@ def step_start_server(context): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: result = sock.connect_ex((context.server_fqdn, context.server_port)) if result == 0: - print("\x1b[33;42mserver started!\x1b[0m") + print("\x1b[33;46mserver started!\x1b[0m") return attempts += 1 if attempts > 20: From 0b0f0565ddabfd426205e45b25c9d4424b68937e Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:33:21 +0100 Subject: [PATCH 58/98] server: tests: ci : build and run tests for all matrix defines, sanitizer and type --- .github/workflows/server-test.yml | 48 +++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index b7a20bbe5e222..52dcaca43be0d 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -19,6 +19,29 @@ jobs: image: ubuntu:latest ports: - 8888 + options: --cpus 4 + + strategy: + matrix: + sanitizer: [ADDRESS, THREAD, UNDEFINED] + build_type: [Debug, Release] + include: + - build: 'noavx' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX=OFF -DLLAMA_AVX2=OFF -DLLAMA_FMA=OFF' + - build: 'avx2' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON' + - build: 'avx' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX2=OFF' + - build: 'avx512' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX512=ON' + - build: 'clblast' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CLBLAST=ON' + - build: 'openblas' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS' + - build: 'kompute' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_KOMPUTE=ON -DKOMPUTE_OPT_DISABLE_VULKAN_VERSION_CHECK=ON' + - build: 'vulkan' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_VULKAN=ON' steps: - name: Clone @@ -36,13 +59,34 @@ jobs: curl \ psmisc + - name: Download CLBlast + id: get_clblast + if: ${{ matrix.build == 'clblast' }} + run: | + apt install libclblast-dev + + - name: Download OpenBLAS + id: get_openblas + if: ${{ matrix.build == 'openblas' }} + run: | + apt-get -y install libopenblas-dev + + - name: Install Vulkan SDK + id: get_vulkan + if: ${{ matrix.build == 'kompute' || matrix.build == 'vulkan' }} + run: | + wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | tee /etc/apt/trusted.gpg.d/lunarg.asc + wget -qO /etc/apt/sources.list.d/lunarg-vulkan-jammy.list http://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list + apt-get update + apt-get -y install vulkan-sdk + - name: Build id: cmake_build run: | mkdir build cd build - cmake .. -DCMAKE_BUILD_TYPE=Release - cmake --build . --config Release -j $(nproc) --target server + cmake .. -DLLAMA_FATAL_WARNINGS=ON -DLLAMA_SANITIZE_${{ matrix.sanitizer }}=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.defines }} + cmake --build . --config ${{ matrix.build_type }} -j $(nproc) --target server - name: Tests dependencies id: test_dependencies From 29f88330588d1a916dc82d8952e7a0e760ea02e2 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:39:45 +0100 Subject: [PATCH 59/98] server: tests: ci : fix wget missing --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 52dcaca43be0d..46bd58a1a8022 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -56,7 +56,7 @@ jobs: build-essential \ cmake \ python3-pip \ - curl \ + wget \ psmisc - name: Download CLBlast From 12bb797193ef18867dcacb41162f803f26f76942 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:41:41 +0100 Subject: [PATCH 60/98] server: tests: ci : add git --- .github/workflows/server-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 46bd58a1a8022..e3aea1140f359 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -54,6 +54,7 @@ jobs: apt-get update apt-get -y install \ build-essential \ + git \ cmake \ python3-pip \ wget \ From 68cd1a4c162b80acad23dd616f38ecb624918ac5 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:46:17 +0100 Subject: [PATCH 61/98] server: tests: ci : matrix cuda --- .github/workflows/server-test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index e3aea1140f359..033509900d75d 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -42,6 +42,8 @@ jobs: defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_KOMPUTE=ON -DKOMPUTE_OPT_DISABLE_VULKAN_VERSION_CHECK=ON' - build: 'vulkan' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_VULKAN=ON' + - build: 'cuda' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON' steps: - name: Clone @@ -81,6 +83,15 @@ jobs: apt-get update apt-get -y install vulkan-sdk + - name: Install CUDA + id: get_vulkan + if: ${{ matrix.build == 'cuda' }} + run: | + wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb + dpkg -i cuda-keyring_1.1-1_all.deb + apt-get update + apt-get install cuda-toolkit + - name: Build id: cmake_build run: | From 86896aadd0c3e0145bc3bf47adee5755eff3da6f Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:53:46 +0100 Subject: [PATCH 62/98] server: tests: ci : continue on error --- .github/workflows/server-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 033509900d75d..894b63c4f0128 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -21,6 +21,8 @@ jobs: - 8888 options: --cpus 4 + continue-on-error: true + strategy: matrix: sanitizer: [ADDRESS, THREAD, UNDEFINED] From 334902b13ee75d31d2e2480668b9aaa687603c5d Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:56:07 +0100 Subject: [PATCH 63/98] server: tests: ci : fix step id duplicated --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 894b63c4f0128..4d512eea1026c 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -86,7 +86,7 @@ jobs: apt-get -y install vulkan-sdk - name: Install CUDA - id: get_vulkan + id: get_cuda if: ${{ matrix.build == 'cuda' }} run: | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb From fce2e00023ece924451fcb32f03160c106cfa49a Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 10:58:59 +0100 Subject: [PATCH 64/98] server: tests: ci : fix cuda install --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 4d512eea1026c..61ca12ffb714b 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -92,7 +92,7 @@ jobs: wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb dpkg -i cuda-keyring_1.1-1_all.deb apt-get update - apt-get install cuda-toolkit + apt-get -y install cuda-toolkit - name: Build id: cmake_build From e4fb79007782e0f15a54f5e96c2b68bf1dbb724f Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:19:49 +0100 Subject: [PATCH 65/98] server: test: ci fix cuda build --- .github/workflows/server-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 61ca12ffb714b..d9a4ac595d0ff 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -44,8 +44,8 @@ jobs: defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_KOMPUTE=ON -DKOMPUTE_OPT_DISABLE_VULKAN_VERSION_CHECK=ON' - build: 'vulkan' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_VULKAN=ON' - - build: 'cuda' - defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON' + - build: 'cublas' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON -DCUDAToolkit_ROOT=/usr/local/cuda' steps: - name: Clone @@ -87,7 +87,7 @@ jobs: - name: Install CUDA id: get_cuda - if: ${{ matrix.build == 'cuda' }} + if: ${{ matrix.build == 'cublas' }} run: | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb dpkg -i cuda-keyring_1.1-1_all.deb From 2edd995f2a5540b3521fe2d6b5680d968c38b00f Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:27:19 +0100 Subject: [PATCH 66/98] server: test: ci fix cublas build --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index d9a4ac595d0ff..d59328fda79aa 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -45,7 +45,7 @@ jobs: - build: 'vulkan' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_VULKAN=ON' - build: 'cublas' - defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON -DCUDAToolkit_ROOT=/usr/local/cuda' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON -DCUDAToolkit_ROOT=/usr/local/cuda -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc' steps: - name: Clone From fa51baca9a7e192d711d0d7c105b9b9050ad4cd6 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:30:24 +0100 Subject: [PATCH 67/98] server: test: ci fix matrix --- .github/workflows/server-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index d59328fda79aa..d48b4e1989945 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -27,6 +27,7 @@ jobs: matrix: sanitizer: [ADDRESS, THREAD, UNDEFINED] build_type: [Debug, Release] + build: [noavx, avx2, avx, avx512, clblast, openblas, kompute, cublas, cublas] include: - build: 'noavx' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX=OFF -DLLAMA_AVX2=OFF -DLLAMA_FMA=OFF' From 606738eeef1f50760121b2380e303d5fdffd06b8 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:32:25 +0100 Subject: [PATCH 68/98] server: test: ci fix clblast --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index d48b4e1989945..1d7649688b584 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -69,7 +69,7 @@ jobs: id: get_clblast if: ${{ matrix.build == 'clblast' }} run: | - apt install libclblast-dev + apt install -y libclblast-dev - name: Download OpenBLAS id: get_openblas From d159e29d4bc9d57c5dc42535ca643974f1b8d273 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:34:22 +0100 Subject: [PATCH 69/98] server: test: ci fix openblas build --- .github/workflows/server-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 1d7649688b584..8c08fe6461ac1 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -59,6 +59,7 @@ jobs: apt-get update apt-get -y install \ build-essential \ + pkg-config \ git \ cmake \ python3-pip \ From 13863ef956c81f2222e06590ce90b3f0e423b090 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:36:21 +0100 Subject: [PATCH 70/98] server: test: ci matrix --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 8c08fe6461ac1..76dcded84d0d2 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -27,7 +27,7 @@ jobs: matrix: sanitizer: [ADDRESS, THREAD, UNDEFINED] build_type: [Debug, Release] - build: [noavx, avx2, avx, avx512, clblast, openblas, kompute, cublas, cublas] + build: [noavx, avx2, avx, avx512, clblast, openblas, kompute, vulkan, cublas] include: - build: 'noavx' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX=OFF -DLLAMA_AVX2=OFF -DLLAMA_FMA=OFF' From 4d3791a4cb907d0da9b1a501dfa208d2a5dbf410 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:43:06 +0100 Subject: [PATCH 71/98] server: test: ci matrix, experimental on matrix avx512 entry which fail test --- .github/workflows/server-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 76dcded84d0d2..f5bcbafcdb617 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -21,8 +21,6 @@ jobs: - 8888 options: --cpus 4 - continue-on-error: true - strategy: matrix: sanitizer: [ADDRESS, THREAD, UNDEFINED] @@ -37,6 +35,7 @@ jobs: defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX2=OFF' - build: 'avx512' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX512=ON' + experimental: true # aiohttp.client_exceptions.ClientOSError: [Errno 104] Connection reset by peer - build: 'clblast' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CLBLAST=ON' - build: 'openblas' @@ -117,6 +116,7 @@ jobs: - name: Server Integration Tests id: server_integration_test + continue-on-error: ${{ matrix.experimental }} run: | cd examples/server/tests PORT=8888 ./tests.sh From b94809b63e68231d65dd814e3b8b311b76a9dfdd Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 11:53:52 +0100 Subject: [PATCH 72/98] server: test: ci cmake remove all warning as it is done by the classical build and does not matter for testing --- .github/workflows/server-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index f5bcbafcdb617..241c928164797 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -100,7 +100,7 @@ jobs: run: | mkdir build cd build - cmake .. -DLLAMA_FATAL_WARNINGS=ON -DLLAMA_SANITIZE_${{ matrix.sanitizer }}=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.defines }} + cmake .. -DLLAMA_SANITIZE_${{ matrix.sanitizer }}=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.defines }} cmake --build . --config ${{ matrix.build_type }} -j $(nproc) --target server - name: Tests dependencies From 5a621e714d3ed42e0e2a9a08187c690afd910156 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 12:04:01 +0100 Subject: [PATCH 73/98] server: test: ci make arch not available pass the test --- .github/workflows/server-test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 241c928164797..e6e710a660353 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -35,15 +35,18 @@ jobs: defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX2=OFF' - build: 'avx512' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX512=ON' - experimental: true # aiohttp.client_exceptions.ClientOSError: [Errno 104] Connection reset by peer + arch_not_available: true - build: 'clblast' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CLBLAST=ON' + arch_not_available: true - build: 'openblas' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS' - build: 'kompute' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_KOMPUTE=ON -DKOMPUTE_OPT_DISABLE_VULKAN_VERSION_CHECK=ON' + arch_not_available: true - build: 'vulkan' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_VULKAN=ON' + arch_not_available: true - build: 'cublas' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON -DCUDAToolkit_ROOT=/usr/local/cuda -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc' @@ -116,7 +119,7 @@ jobs: - name: Server Integration Tests id: server_integration_test - continue-on-error: ${{ matrix.experimental }} + continue-on-error: ${{ matrix.experimental || matrix.arch_not_available }} run: | cd examples/server/tests PORT=8888 ./tests.sh From 54ea4d4d8c83814d7a938664cf08a6d994bd43c0 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 12:28:59 +0100 Subject: [PATCH 74/98] server: test: ax512 experimental --- .github/workflows/server-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index e6e710a660353..9b63b42644fb2 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -13,7 +13,7 @@ on: paths: ['**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m', 'examples/server/**.*'] jobs: - ubuntu-latest-cmake: + ubuntu: runs-on: ubuntu-latest container: image: ubuntu:latest @@ -35,7 +35,7 @@ jobs: defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX2=OFF' - build: 'avx512' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX512=ON' - arch_not_available: true + experimental: true - build: 'clblast' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CLBLAST=ON' arch_not_available: true From 5b2ce45d570730fe88f4f7c70d476e08221c8cc7 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 12:32:30 +0100 Subject: [PATCH 75/98] server: test: display server logs in case of failure --- examples/server/tests/features/environment.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index de2f9c9828d02..05ede64637633 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -1,4 +1,3 @@ -import multiprocessing import os import socket import subprocess @@ -17,6 +16,16 @@ def before_scenario(context, scenario): def after_scenario(context, scenario): + if scenario.status == "failed": + print(f"\x1b[33;101mSCENARIO FAILED: {scenario.name} server logs:\x1b[0m\n\n") + if os.path.isfile('llama.log'): + with closing(open('llama.log', 'r')) as f: + for line in f: + print(line) + + if not pid_exists(context.server_process.pid): + assert False, f"Server not running pid={context.server_process.pid} ..." + print(f"stopping server pid={context.server_process.pid} ...") context.server_process.kill() # Wait few for socket to free up @@ -40,3 +49,16 @@ def is_server_listening(server_fqdn, server_port): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: result = sock.connect_ex((server_fqdn, server_port)) return result == 0 + + +def pid_exists(pid): + """Check whether pid exists in the current process table.""" + import errno + if pid < 0: + return False + try: + os.kill(pid, 0) + except OSError as e: + return e.errno == errno.EPERM + else: + return True From 6dc3af5432fc63a856fff10555f3f97925d86d6d Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 12:42:51 +0100 Subject: [PATCH 76/98] server: test: fix CUDA LD PATH --- .github/workflows/server-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 9b63b42644fb2..40f8b6d80ceda 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -121,5 +121,6 @@ jobs: id: server_integration_test continue-on-error: ${{ matrix.experimental || matrix.arch_not_available }} run: | + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/x86_64-linux-gnu/:/usr/local/cuda/targets/x86_64-linux/lib/" cd examples/server/tests PORT=8888 ./tests.sh From 83c386f237453ed3010dd2e202c687e2e1727609 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 12:51:49 +0100 Subject: [PATCH 77/98] server: test: ci debug LD path --- .github/workflows/server-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 40f8b6d80ceda..bc39eb5abc695 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -97,6 +97,7 @@ jobs: dpkg -i cuda-keyring_1.1-1_all.deb apt-get update apt-get -y install cuda-toolkit + find / -type f -name "*cuda*so*" - name: Build id: cmake_build @@ -105,6 +106,7 @@ jobs: cd build cmake .. -DLLAMA_SANITIZE_${{ matrix.sanitizer }}=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.defines }} cmake --build . --config ${{ matrix.build_type }} -j $(nproc) --target server + ldd bin/server - name: Tests dependencies id: test_dependencies From 0d380aefc35c49e22badd22348fc594531f09cd7 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 13:04:43 +0100 Subject: [PATCH 78/98] server: test: ci debug CI LD path --- .github/workflows/server-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index bc39eb5abc695..b8633e4b42853 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -96,7 +96,7 @@ jobs: wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb dpkg -i cuda-keyring_1.1-1_all.deb apt-get update - apt-get -y install cuda-toolkit + apt-get -y install cuda-toolkit nvidia-gds find / -type f -name "*cuda*so*" - name: Build @@ -123,6 +123,6 @@ jobs: id: server_integration_test continue-on-error: ${{ matrix.experimental || matrix.arch_not_available }} run: | - export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/x86_64-linux-gnu/:/usr/local/cuda/targets/x86_64-linux/lib/" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/x86_64-linux-gnu/:/usr/local/cuda/lib64:/usr/local/cuda/targets/x86_64-linux/lib/" cd examples/server/tests PORT=8888 ./tests.sh From c75e0e106b5a5c87b47a89ffb9900647ddcb70e2 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 13:18:37 +0100 Subject: [PATCH 79/98] server: test: ci switch to nvidia based docker image for cuda --- .github/workflows/server-test.yml | 40 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index b8633e4b42853..6aec537f83837 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -13,42 +13,52 @@ on: paths: ['**/CMakeLists.txt', '**/Makefile', '**/*.h', '**/*.hpp', '**/*.c', '**/*.cpp', '**/*.cu', '**/*.swift', '**/*.m', 'examples/server/**.*'] jobs: - ubuntu: + server: runs-on: ubuntu-latest - container: - image: ubuntu:latest - ports: - - 8888 - options: --cpus 4 strategy: matrix: sanitizer: [ADDRESS, THREAD, UNDEFINED] build_type: [Debug, Release] - build: [noavx, avx2, avx, avx512, clblast, openblas, kompute, vulkan, cublas] + build: [noavx, avx2, avx, avx512, cublas, clblast, openblas, kompute, vulkan] include: - build: 'noavx' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX=OFF -DLLAMA_AVX2=OFF -DLLAMA_FMA=OFF' + image: ubuntu:latest - build: 'avx2' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON' + image: ubuntu:latest - build: 'avx' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX2=OFF' + image: ubuntu:latest - build: 'avx512' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX512=ON' + image: ubuntu:latest experimental: true + - build: 'cublas' + defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON' + image: nvidia/cuda:12.3.1-devel-ubuntu22.04 - build: 'clblast' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CLBLAST=ON' + image: ubuntu:latest arch_not_available: true - build: 'openblas' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS' + image: ubuntu:latest - build: 'kompute' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_KOMPUTE=ON -DKOMPUTE_OPT_DISABLE_VULKAN_VERSION_CHECK=ON' + image: ubuntu:latest arch_not_available: true - build: 'vulkan' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_VULKAN=ON' + image: ubuntu:latest arch_not_available: true - - build: 'cublas' - defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON -DCUDAToolkit_ROOT=/usr/local/cuda -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc' + + container: + image: ${{ matrix.image }} + ports: + - 8888 + options: --cpus 4 steps: - name: Clone @@ -89,16 +99,6 @@ jobs: apt-get update apt-get -y install vulkan-sdk - - name: Install CUDA - id: get_cuda - if: ${{ matrix.build == 'cublas' }} - run: | - wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb - dpkg -i cuda-keyring_1.1-1_all.deb - apt-get update - apt-get -y install cuda-toolkit nvidia-gds - find / -type f -name "*cuda*so*" - - name: Build id: cmake_build run: | @@ -106,7 +106,6 @@ jobs: cd build cmake .. -DLLAMA_SANITIZE_${{ matrix.sanitizer }}=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.defines }} cmake --build . --config ${{ matrix.build_type }} -j $(nproc) --target server - ldd bin/server - name: Tests dependencies id: test_dependencies @@ -123,6 +122,5 @@ jobs: id: server_integration_test continue-on-error: ${{ matrix.experimental || matrix.arch_not_available }} run: | - export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/x86_64-linux-gnu/:/usr/local/cuda/lib64:/usr/local/cuda/targets/x86_64-linux/lib/" cd examples/server/tests PORT=8888 ./tests.sh From 2c8bf2407bc2894e70974aed7c1506fea183e143 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 13:32:39 +0100 Subject: [PATCH 80/98] server: test: ci give up with nvidia as it requires the nvidia docker runtime --- .github/workflows/server-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 6aec537f83837..dfecc471a1835 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -38,6 +38,7 @@ jobs: - build: 'cublas' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CUBLAS=ON' image: nvidia/cuda:12.3.1-devel-ubuntu22.04 + arch_not_available: true # require nvidia docker engine - build: 'clblast' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_CLBLAST=ON' image: ubuntu:latest From 777bdcf58f712732bb2f55801cc2830061e7baa8 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 13:44:37 +0100 Subject: [PATCH 81/98] server: test: ci rename step name to Test, change matrix order for better clarity --- .github/workflows/server-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index dfecc471a1835..ece262f3fa959 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -18,9 +18,9 @@ jobs: strategy: matrix: + build: [noavx, avx2, avx, avx512, cublas, clblast, openblas, kompute, vulkan] sanitizer: [ADDRESS, THREAD, UNDEFINED] build_type: [Debug, Release] - build: [noavx, avx2, avx, avx512, cublas, clblast, openblas, kompute, vulkan] include: - build: 'noavx' defines: '-DLLAMA_NATIVE=OFF -DLLAMA_BUILD_SERVER=ON -DLLAMA_AVX=OFF -DLLAMA_AVX2=OFF -DLLAMA_FMA=OFF' @@ -113,13 +113,13 @@ jobs: run: | pip install -r examples/server/tests/requirements.txt - - name: Download test model - id: download_model + - name: Download models + id: download_models run: | cd examples/server/tests ../../../scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf - - name: Server Integration Tests + - name: Tests id: server_integration_test continue-on-error: ${{ matrix.experimental || matrix.arch_not_available }} run: | From e10b83a2176209b25c3877378eb2f125ed25ec68 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 13:54:19 +0100 Subject: [PATCH 82/98] server: test: ci rename job name to Server --- .github/workflows/server-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index ece262f3fa959..ed27dc528fb61 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -1,5 +1,5 @@ -# Server test scenario -name: Server Integration Tests +# Server build and tests +name: Server on: workflow_dispatch: # allows manual triggering From 4d27466ca5534e39ebb7a9d3387d821936462272 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 14:44:12 +0100 Subject: [PATCH 83/98] server: tests: move all requests call to asyncio --- examples/server/tests/features/steps/steps.py | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index cfe03e799c2e7..c8be94eb80e2b 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -10,7 +10,6 @@ import aiohttp import openai -import requests from behave import step from behave.api.async_step import async_run_until_complete @@ -75,6 +74,7 @@ def step_server_n_predict(context, n_predict): def step_server_continuous_batching(context): context.server_continuous_batching = True + @step(u'embeddings extraction') def step_server_embeddings(context): context.server_embeddings = True @@ -306,12 +306,16 @@ async def all_prompts_are_predicted(context, expected_predicted_n=None): @step(u'embeddings are computed for') -def step_compute_embedding(context): - response = requests.post(f'{context.base_url}/embedding', json={ - "content": context.text, - }) - assert response.status_code == 200 - context.embeddings = response.json()['embedding'] +@async_run_until_complete +async def step_compute_embedding(context): + async with aiohttp.ClientSession() as session: + async with session.post(f'{context.base_url}/embedding', + json={ + "content": context.text, + }) as response: + assert response.status == 200 + response_json = await response.json() + context.embeddings = response_json['embedding'] @step(u'embeddings are generated') @@ -338,32 +342,42 @@ def step_oai_compute_embedding(context): @step(u'tokenizing') -def step_tokenize(context): +@async_run_until_complete +async def step_tokenize(context): context.tokenized_text = context.text - response = requests.post(f'{context.base_url}/tokenize', json={ - "content": context.tokenized_text, - }) - assert response.status_code == 200 - context.tokens = response.json()['tokens'] + async with aiohttp.ClientSession() as session: + async with session.post(f'{context.base_url}/tokenize', + json={ + "content": context.tokenized_text, + }) as response: + assert response.status == 200 + tokenize_json = await response.json() + context.tokens = tokenize_json['tokens'] @step(u'tokens can be detokenize') -def step_detokenize(context): +@async_run_until_complete +async def step_detokenize(context): assert len(context.tokens) > 0 - response = requests.post(f'{context.base_url}/detokenize', json={ - "tokens": context.tokens, - }) - assert response.status_code == 200 - # SPM tokenizer adds a whitespace prefix: https://github.com/google/sentencepiece/issues/15 - assert context.tokenized_text == response.json()['content'].strip() + async with aiohttp.ClientSession() as session: + async with session.post(f'{context.base_url}/detokenize', + json={ + "tokens": context.tokens, + }) as response: + assert response.status == 200 + detokenize_json = await response.json() + # SPM tokenizer adds a whitespace prefix: https://github.com/google/sentencepiece/issues/15 + assert context.tokenized_text == detokenize_json['content'].strip() @step(u'an OPTIONS request is sent from {origin}') -def step_options_request(context, origin): - options_response = requests.options(f'{context.base_url}/v1/chat/completions', - headers={"Origin": origin}) - assert options_response.status_code == 200 - context.options_response = options_response +@async_run_until_complete +async def step_options_request(context, origin): + async with aiohttp.ClientSession() as session: + async with session.options(f'{context.base_url}/v1/chat/completions', + headers={"Origin": origin}) as response: + assert response.status == 200 + context.options_response = response @step(u'CORS header {cors_header} is set to {cors_header_value}') From 1c1fd405764f3cea8a8053f248d7c30762a7925d Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 17:12:16 +0100 Subject: [PATCH 84/98] server: tests: allow to pass argument to the test file add wrong_usage.feature to demonstrate user issue which will not be fixed. --- examples/server/tests/README.md | 11 ++++++---- .../server/tests/features/wrong_usage.feature | 22 +++++++++++++++++++ examples/server/tests/tests.sh | 10 +++++++-- 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 examples/server/tests/features/wrong_usage.feature diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 1892bafc929f0..6ebff8674510e 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -15,9 +15,12 @@ It's possible to override some scenario steps values with environment variables: - `$PORT` -> `context.server_port` to set the listening port of the server during scenario, default: `8080` - `$LLAMA_SERVER_BIN_PATH` -> to change the server binary path, default: `../../../build/bin/server` -To change the server path, use `LLAMA_SERVER_BIN_PATH` environment variable. +### Run @bug, @wip or @wrong_usage annotated scenario -### Skipped scenario +Feature or Scenario must be annotated with `@llama.cpp` to be included in the default scope. +- `@bug` annotation aims to link a scenario with a GitHub issue. +- `@wrong_usage` are meant to show user issue that are actually an expected behavior +- `@wip` to focus on a scenario working in progress -Feature or Scenario must be annotated with `@llama.cpp` to be included in the scope. -`@bug` annotation aims to link a scenario with a GitHub issue. +To run a scenario annotated with `@bug`, start: +`./tests.sh --tags bug` \ No newline at end of file diff --git a/examples/server/tests/features/wrong_usage.feature b/examples/server/tests/features/wrong_usage.feature new file mode 100644 index 0000000000000..59098a2901cd6 --- /dev/null +++ b/examples/server/tests/features/wrong_usage.feature @@ -0,0 +1,22 @@ +# run with ./test.sh --tags wrong_usage +@wrong_usage +Feature: Wrong usage of llama.cpp server + + #3969 The user must always set --n-predict option + # to cap the number of tokens any completion request can generate + # or pass n_predict or max_tokens in the request. + Scenario: Infinite loop + Given a server listening on localhost:8080 + And a model file stories260K.gguf + And 1 slots + And 32 KV cache size + # Uncomment below to fix the issue + #And 64 server max tokens to predict + Then the server is starting + Given a prompt: + """ + Go to: infinite loop + """ + Given concurrent completion requests + + Then all prompts are predicted \ No newline at end of file diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index 3b101ad3d97e2..a2e29748d2b66 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -2,5 +2,11 @@ set -eu -# Start @llama.cpp scenario -behave --summary --stop --no-capture --tags llama.cpp +if [ $# -lt 1 ] +then + # Start @llama.cpp scenario + behave --summary --stop --no-capture --tags llama.cpp +else + behave "$@" +fi + From 2109743fe3eedb967e064e37d939a51ec335dff5 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 17:12:33 +0100 Subject: [PATCH 85/98] server: tests: print server logs only on github action --- examples/server/tests/features/environment.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index 05ede64637633..eee7d7a17b14c 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -17,11 +17,12 @@ def before_scenario(context, scenario): def after_scenario(context, scenario): if scenario.status == "failed": - print(f"\x1b[33;101mSCENARIO FAILED: {scenario.name} server logs:\x1b[0m\n\n") - if os.path.isfile('llama.log'): - with closing(open('llama.log', 'r')) as f: - for line in f: - print(line) + if 'GITHUB_ACTIONS' in os.environ: + print(f"\x1b[33;101mSCENARIO FAILED: {scenario.name} server logs:\x1b[0m\n\n") + if os.path.isfile('llama.log'): + with closing(open('llama.log', 'r')) as f: + for line in f: + print(line) if not pid_exists(context.server_process.pid): assert False, f"Server not running pid={context.server_process.pid} ..." From 30f802d0d7a8a3cc82052dc7cfc1abf46f85b35d Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 18:28:05 +0100 Subject: [PATCH 86/98] server: tests: check if the server has not crashed after a scenario --- examples/server/tests/features/environment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index eee7d7a17b14c..690571d238d23 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -23,6 +23,8 @@ def after_scenario(context, scenario): with closing(open('llama.log', 'r')) as f: for line in f: print(line) + if not is_server_listening(context.server_fqdn, context.server_port): + print("ERROR: Server has crashed") if not pid_exists(context.server_process.pid): assert False, f"Server not running pid={context.server_process.pid} ..." From 6c0e6f4f9cd30b04335c8b1998f7e8145ffd8809 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 18:41:11 +0100 Subject: [PATCH 87/98] server: tests: adding concurrent embedding in issue #5655 allow to enable VERBOSE mode --- .../workflows/{server-test.yml => server.yml} | 0 examples/server/tests/README.md | 7 +- examples/server/tests/features/environment.py | 4 +- examples/server/tests/features/issues.feature | 36 ++++++ examples/server/tests/features/steps/steps.py | 109 +++++++++++------- ...ong_usage.feature => wrong_usages.feature} | 9 +- examples/server/tests/tests.sh | 2 +- 7 files changed, 117 insertions(+), 50 deletions(-) rename .github/workflows/{server-test.yml => server.yml} (100%) create mode 100644 examples/server/tests/features/issues.feature rename examples/server/tests/features/{wrong_usage.feature => wrong_usages.feature} (77%) diff --git a/.github/workflows/server-test.yml b/.github/workflows/server.yml similarity index 100% rename from .github/workflows/server-test.yml rename to .github/workflows/server.yml diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 6ebff8674510e..8f708f8ab0670 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -12,8 +12,9 @@ Server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_ 3. Start the test: `./tests.sh` It's possible to override some scenario steps values with environment variables: - - `$PORT` -> `context.server_port` to set the listening port of the server during scenario, default: `8080` - - `$LLAMA_SERVER_BIN_PATH` -> to change the server binary path, default: `../../../build/bin/server` + - `PORT` -> `context.server_port` to set the listening port of the server during scenario, default: `8080` + - `LLAMA_SERVER_BIN_PATH` -> to change the server binary path, default: `../../../build/bin/server` + - `DEBUG` -> "ON" to enable server verbose mode `--verbose` ### Run @bug, @wip or @wrong_usage annotated scenario @@ -23,4 +24,4 @@ Feature or Scenario must be annotated with `@llama.cpp` to be included in the de - `@wip` to focus on a scenario working in progress To run a scenario annotated with `@bug`, start: -`./tests.sh --tags bug` \ No newline at end of file +`DEBUG=ON ./tests.sh --no-skipped --tags bug` \ No newline at end of file diff --git a/examples/server/tests/features/environment.py b/examples/server/tests/features/environment.py index 690571d238d23..13cc841017f62 100644 --- a/examples/server/tests/features/environment.py +++ b/examples/server/tests/features/environment.py @@ -24,7 +24,7 @@ def after_scenario(context, scenario): for line in f: print(line) if not is_server_listening(context.server_fqdn, context.server_port): - print("ERROR: Server has crashed") + print("\x1b[33;101mERROR: Server stopped listening\x1b[0m") if not pid_exists(context.server_process.pid): assert False, f"Server not running pid={context.server_process.pid} ..." @@ -41,7 +41,7 @@ def after_scenario(context, scenario): time.sleep(0.1) attempts += 1 if attempts > 5: - print(f"Server dandling exits, killing all {context.server_path} ...") + print(f"Server dangling exits, killing all {context.server_path} ...") process = subprocess.run(['killall', '-9', context.server_path], stderr=subprocess.PIPE, universal_newlines=True) diff --git a/examples/server/tests/features/issues.feature b/examples/server/tests/features/issues.feature new file mode 100644 index 0000000000000..542006d9a8df2 --- /dev/null +++ b/examples/server/tests/features/issues.feature @@ -0,0 +1,36 @@ +# List of ongoing issues +@bug +Feature: Issues + # Issue #5655 + Scenario: Multi users embeddings + Given a server listening on localhost:8080 + And a model file stories260K.gguf + And a model alias tinyllama-2 + And 42 as server seed + And 64 KV cache size + And 2 slots + And continuous batching + And embeddings extraction + Then the server is starting + Then the server is healthy + + Given a prompt: + """ + Write a very long story about AI. + """ + And a prompt: + """ + Write another very long music lyrics. + """ + And a prompt: + """ + Write a very long poem. + """ + And a prompt: + """ + Write a very long joke. + """ + Given concurrent embedding requests + Then the server is busy + Then the server is idle + Then all embeddings are generated diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index c8be94eb80e2b..5e2b729eb120c 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -35,8 +35,8 @@ def step_server_config(context, server_fqdn, server_port): context.server_seed = None context.user_api_key = None - context.completions = [] - context.concurrent_completion_tasks = [] + context.tasks_result = [] + context.concurrent_tasks = [] context.prompts = [] @@ -149,7 +149,7 @@ async def step_request_completion(context, api_error): server_seed=context.server_seed, expect_api_error=expect_api_error, user_api_key=context.user_api_key) - context.completions.append(completion) + context.tasks_result.append(completion) print(f"Completion response: {completion}") if expect_api_error: assert completion == 401, f"completion must be an 401 status code: {completion}" @@ -157,12 +157,12 @@ async def step_request_completion(context, api_error): @step(u'{predicted_n} tokens are predicted matching {re_content}') def step_n_tokens_predicted_with_content(context, predicted_n, re_content): - assert_n_tokens_predicted(context.completions.pop(), int(predicted_n), re_content) + assert_n_tokens_predicted(context.tasks_result.pop(), int(predicted_n), re_content) @step(u'{predicted_n} tokens are predicted') def step_n_tokens_predicted(context, predicted_n): - assert_n_tokens_predicted(context.completions.pop(), int(predicted_n)) + assert_n_tokens_predicted(context.tasks_result.pop(), int(predicted_n)) @step(u'a user prompt {user_prompt}') @@ -195,13 +195,13 @@ def step_user_api_key(context, user_api_key): context.user_api_key = user_api_key -@step(u'a user api key ') +@step(u'no user api key') def step_no_user_api_key(context): context.user_api_key = None -@step(u'no user api key') -def step_no_user_api_key(context): +@step(u'a user api key ') +def step_no_user_api_key_space(context): context.user_api_key = None @@ -234,7 +234,7 @@ async def step_oai_chat_completions(context, api_error): if hasattr(context, 'user_api_key') else None, expect_api_error=expect_api_error) - context.completions.append(completion) + context.tasks_result.append(completion) print(f"Completion response: {completion}") if expect_api_error: assert completion == 401, f"completion must be an 401 status code: {completion}" @@ -285,47 +285,38 @@ async def step_oai_chat_completions(context): if hasattr(context, 'user_api_key') else None) -@async_run_until_complete @step(u'all prompts are predicted') -async def step_impl(context): +@async_run_until_complete +async def step_all_prompts_are_predicted(context): await all_prompts_are_predicted(context) @step(u'all prompts are predicted with {n_predict} tokens') @async_run_until_complete -async def step_all_prompts_are_predicted(context, n_predict): +async def step_all_prompts_are_predicted_with_n_tokens(context, n_predict): expected_predicted_n = int(n_predict) await all_prompts_are_predicted(context, expected_predicted_n) async def all_prompts_are_predicted(context, expected_predicted_n=None): - n_completions = await gather_concurrent_completions_tasks(context) + n_completions = await gather_tasks_results(context) assert n_completions > 0 for i in range(n_completions): - assert_n_tokens_predicted(context.completions.pop(), expected_predicted_n=expected_predicted_n) + assert_n_tokens_predicted(context.tasks_result.pop(), expected_predicted_n=expected_predicted_n) + assert len(context.concurrent_tasks) == 0, f"{len(context.concurrent_tasks)} pending requests" @step(u'embeddings are computed for') @async_run_until_complete async def step_compute_embedding(context): - async with aiohttp.ClientSession() as session: - async with session.post(f'{context.base_url}/embedding', - json={ - "content": context.text, - }) as response: - assert response.status == 200 - response_json = await response.json() - context.embeddings = response_json['embedding'] + content = context.text + base_url = context.base_url + context.embeddings = await request_embedding(content, base_url) @step(u'embeddings are generated') -def step_compute_embeddings(context): - assert len(context.embeddings) > 0 - embeddings_computed = False - for emb in context.embeddings: - if emb != 0: - embeddings_computed = True - assert embeddings_computed, f"Embeddings: {context.embeddings}" +def step_assert_embeddings(context): + assert_embeddings(context.embeddings) @step(u'an OAI compatible embeddings computation request for') @@ -341,6 +332,24 @@ def step_oai_compute_embedding(context): context.embeddings = embeddings +@step(u'concurrent embedding requests') +@async_run_until_complete() +async def step_concurrent_embedding_requests(context): + await concurrent_completion_requests(context, + request_embedding, + # prompt is inserted automatically + context.base_url) + + +@step(u'all embeddings are generated') +@async_run_until_complete() +async def all_embeddings_are_generated(context): + n_embedding_requests = await gather_tasks_results(context) + assert n_embedding_requests > 0 + for i in range(n_embedding_requests): + assert_embeddings(context.tasks_result.pop()) + + @step(u'tokenizing') @async_run_until_complete async def step_tokenize(context): @@ -391,7 +400,7 @@ async def concurrent_completion_requests(context, f_completion, *args, **kwargs) assert n_prompts > 0 for prompt_no in range(n_prompts): shifted_args = [context.prompts.pop(), *args] - context.concurrent_completion_tasks.append(asyncio.create_task(f_completion(*shifted_args, **kwargs))) + context.concurrent_tasks.append(asyncio.create_task(f_completion(*shifted_args, **kwargs))) await asyncio.sleep(0.1) @@ -540,6 +549,17 @@ async def oai_chat_completions(user_prompt, return completion_response +async def request_embedding(content, base_url): + async with aiohttp.ClientSession() as session: + async with session.post(f'{base_url}/embedding', + json={ + "content": content, + }) as response: + assert response.status == 200 + response_json = await response.json() + return response_json['embedding'] + + def assert_n_tokens_predicted(completion_response, expected_predicted_n=None, re_content=None): content = completion_response['content'] n_predicted = completion_response['timings']['predicted_n'] @@ -554,12 +574,12 @@ def assert_n_tokens_predicted(completion_response, expected_predicted_n=None, re f' ```\n{content}\n``` do not match /{re_content}/') -async def gather_concurrent_completions_tasks(context): - n_completion_tasks = len(context.concurrent_completion_tasks) - print(f"Waiting for all {n_completion_tasks} completion responses...") - for task_no in range(n_completion_tasks): - context.completions.append(await context.concurrent_completion_tasks.pop()) - n_completions = len(context.completions) +async def gather_tasks_results(context): + n_tasks = len(context.concurrent_tasks) + print(f"Waiting for all {n_tasks} tasks results...") + for task_no in range(n_tasks): + context.tasks_result.append(await context.concurrent_tasks.pop()) + n_completions = len(context.tasks_result) return n_completions @@ -602,16 +622,25 @@ async def wait_for_health_status(context, if counter >= timeout: # Sometimes health requests are triggered after completions are predicted if expected_http_status_code == 503: - if len(context.completions) == 0: - print("\x1b[5;37;43mWARNING: forcing concurrents completions tasks," + if len(context.tasks_result) == 0: + print("\x1b[5;37;43mWARNING: forcing concurrent tasks," " busy health check missed, probably too fast inference\x1b[0m") - n_completions = await gather_concurrent_completions_tasks(context) + n_completions = await gather_tasks_results(context) if n_completions > 0: return assert False, 'timeout exceeded' +def assert_embeddings(embeddings): + assert len(embeddings) > 0 + embeddings_computed = False + for emb in embeddings: + if emb != 0: + embeddings_computed = True + assert embeddings_computed, f"Embeddings: {embeddings}" + + async def request_slots_status(context, expected_slots): async with aiohttp.ClientSession() as session: async with await session.get(f'{context.base_url}/slots') as slots_response: @@ -652,6 +681,8 @@ def start_server_background(context): server_args.extend(['--n-predict', context.n_server_predict]) if context.server_api_key is not None: server_args.extend(['--api-key', context.server_api_key]) + if 'DEBUG' in os.environ and os.environ['DEBUG'] == 'ON': + server_args.append('--verbose') print(f"starting server with: {context.server_path}", *server_args) context.server_process = subprocess.Popen( [str(arg) for arg in [context.server_path, *server_args]], diff --git a/examples/server/tests/features/wrong_usage.feature b/examples/server/tests/features/wrong_usages.feature similarity index 77% rename from examples/server/tests/features/wrong_usage.feature rename to examples/server/tests/features/wrong_usages.feature index 59098a2901cd6..e228b2371ccce 100644 --- a/examples/server/tests/features/wrong_usage.feature +++ b/examples/server/tests/features/wrong_usages.feature @@ -4,12 +4,10 @@ Feature: Wrong usage of llama.cpp server #3969 The user must always set --n-predict option # to cap the number of tokens any completion request can generate - # or pass n_predict or max_tokens in the request. + # or pass n_predict/max_tokens in the request. Scenario: Infinite loop Given a server listening on localhost:8080 And a model file stories260K.gguf - And 1 slots - And 32 KV cache size # Uncomment below to fix the issue #And 64 server max tokens to predict Then the server is starting @@ -17,6 +15,7 @@ Feature: Wrong usage of llama.cpp server """ Go to: infinite loop """ + # Uncomment below to fix the issue + #And 128 max tokens to predict Given concurrent completion requests - - Then all prompts are predicted \ No newline at end of file + Then all prompts are predicted diff --git a/examples/server/tests/tests.sh b/examples/server/tests/tests.sh index a2e29748d2b66..17a4e6fc64307 100755 --- a/examples/server/tests/tests.sh +++ b/examples/server/tests/tests.sh @@ -5,7 +5,7 @@ set -eu if [ $# -lt 1 ] then # Start @llama.cpp scenario - behave --summary --stop --no-capture --tags llama.cpp + behave --summary --stop --no-capture --exclude 'issues|wrong_usages' --tags llama.cpp else behave "$@" fi From 77b8589dbb4a7416f383de86d59eb0d153ee85d2 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 18:57:38 +0100 Subject: [PATCH 88/98] server: tests: linter --- examples/server/tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 8f708f8ab0670..aeac7f1696662 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -24,4 +24,4 @@ Feature or Scenario must be annotated with `@llama.cpp` to be included in the de - `@wip` to focus on a scenario working in progress To run a scenario annotated with `@bug`, start: -`DEBUG=ON ./tests.sh --no-skipped --tags bug` \ No newline at end of file +`DEBUG=ON ./tests.sh --no-skipped --tags bug` From 71831494b11ee4e7f6bfdd5232dcb4a0a33d826c Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 19:28:06 +0100 Subject: [PATCH 89/98] server: tests: fix concurrent OAI streaming request --- .../server/tests/features/parallel.feature | 2 +- examples/server/tests/features/steps/steps.py | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/examples/server/tests/features/parallel.feature b/examples/server/tests/features/parallel.feature index 07be39ef58a4b..802d624ffc9a3 100644 --- a/examples/server/tests/features/parallel.feature +++ b/examples/server/tests/features/parallel.feature @@ -51,7 +51,7 @@ Feature: Parallel Examples: | streaming | n_predict | | disabled | 128 | - #| enabled | 64 | FIXME: phymbert: need to investigate why in aiohttp with streaming only one token is generated + | enabled | 64 | Scenario: Multi users with total number of tokens to predict exceeds the KV Cache size #3969 Given a prompt: diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 5e2b729eb120c..fda8aab8fae30 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -485,20 +485,25 @@ async def oai_chat_completions(user_prompt, assert response.status == 200 assert response.headers['Access-Control-Allow-Origin'] == origin assert response.headers['Content-Type'] == "text/event-stream" - - async for line_in_bytes in response.content: - line = line_in_bytes.decode('utf8') - event_data = line.split(': ', 1) - assert event_data[0] == 'data', f'{event_data}' - chunk_raw = event_data[1] - - chunk = json.loads(chunk_raw) - assert len(chunk['choices']) == 1 - delta = chunk['choices'][0]['delta'] - if 'content' in delta: - completion_response['content'] += delta['content'] - completion_response['timings']['predicted_n'] += 1 - print(f"DEBUG completion_response: {completion_response}") + event_received = True + while event_received: + event_received = False + async for line_in_bytes in response.content: + line = line_in_bytes.decode('utf8') + line = line.rstrip('\n').rstrip('\r') + if line == '': + continue + event_data = line.split(': ', 1) + assert event_data[0] == 'data', f'Bad event code received: ```{event_data}```' + chunk_raw = event_data[1] + + chunk = json.loads(chunk_raw) + assert len(chunk['choices']) == 1, f"no choices provided, line ```{line}```" + delta = chunk['choices'][0]['delta'] + if 'content' in delta: + completion_response['content'] += delta['content'] + completion_response['timings']['predicted_n'] += 1 + print(f"DEBUG completion_response: {completion_response}") else: if expect_api_error is None or not expect_api_error: assert response.status == 200 From 2d107babc49c7336ca2330da3dcd55bc4a523f64 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Fri, 23 Feb 2024 22:25:39 +0100 Subject: [PATCH 90/98] server: tests: add a note regarding inference speed. --- examples/server/tests/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index aeac7f1696662..fb59cf331797e 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -1,6 +1,12 @@ -# Server Integration Test +# Server tests -Server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) with [behave](https://behave.readthedocs.io/en/latest/). +Python based server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) and [behave](https://behave.readthedocs.io/en/latest/). + +Tests target GitHub workflows job runners with 4 vCPU. + +Requests are using [aiohttp](https://docs.aiohttp.org/en/stable/client_reference.html), [asyncio](https://docs.python.org/fr/3/library/asyncio.html) based http client. + +Note: If the host architecture inference speed is faster than GitHub runners one, parallel scenario may randomly fail. To mitigate it, you can increase values in `n_predict`, `kv_size`. ### Install dependencies `pip install -r requirements.txt` @@ -14,7 +20,7 @@ Server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_ It's possible to override some scenario steps values with environment variables: - `PORT` -> `context.server_port` to set the listening port of the server during scenario, default: `8080` - `LLAMA_SERVER_BIN_PATH` -> to change the server binary path, default: `../../../build/bin/server` - - `DEBUG` -> "ON" to enable server verbose mode `--verbose` + - `DEBUG` -> "ON" to enable server verbose mode `--verbose` ### Run @bug, @wip or @wrong_usage annotated scenario From 124ca773c6a0d9af90e4507c88a56eb1902dc2fc Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 08:23:19 +0100 Subject: [PATCH 91/98] server: tests: removing debug print --- examples/server/tests/features/steps/steps.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index fda8aab8fae30..36c815d66f1aa 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -480,8 +480,6 @@ async def oai_chat_completions(user_prompt, json=payload, headers=headers) as response: if enable_streaming: - # FIXME: does not work; the server is generating only one token - print("DEBUG payload", payload) assert response.status == 200 assert response.headers['Access-Control-Allow-Origin'] == origin assert response.headers['Content-Type'] == "text/event-stream" @@ -503,7 +501,6 @@ async def oai_chat_completions(user_prompt, if 'content' in delta: completion_response['content'] += delta['content'] completion_response['timings']['predicted_n'] += 1 - print(f"DEBUG completion_response: {completion_response}") else: if expect_api_error is None or not expect_api_error: assert response.status == 200 From 5957a2dbcb353a2d8767fa7604aa39a0d4a0924e Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 10:55:20 +0100 Subject: [PATCH 92/98] server: tests - allow print on debug --- examples/server/tests/features/steps/steps.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index 36c815d66f1aa..bbe7c3f6cae25 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -24,6 +24,7 @@ def step_server_config(context, server_fqdn, server_port): context.base_url = f'http://{context.server_fqdn}:{context.server_port}' + context.debug = 'DEBUG' in os.environ and os.environ['DEBUG'] == 'ON' context.model_alias = None context.n_ctx = None context.n_predict = None @@ -593,7 +594,8 @@ async def wait_for_health_status(context, slots_idle=None, slots_processing=None, expected_slots=None): - print(f"Starting checking for health for expected_health_status={expected_health_status}") + if context.debug: + print(f"Starting checking for health for expected_health_status={expected_health_status}") timeout = 3 # seconds interval = 0.5 counter = 0 @@ -602,8 +604,9 @@ async def wait_for_health_status(context, async with await session.get(f'{base_url}/health', params=params) as health_response: status_code = health_response.status health = await health_response.json() - print(f"HEALTH - response for expected health status='{expected_health_status}' on " - f"'{base_url}/health'?{params} is {health}") + if context.debug: + print(f"HEALTH - response for expected health status='{expected_health_status}' on " + f"'{base_url}/health'?{params} is {health}") if (status_code == expected_http_status_code and health['status'] == expected_health_status and (slots_idle is None or health['slots_idle'] == slots_idle) @@ -683,7 +686,7 @@ def start_server_background(context): server_args.extend(['--n-predict', context.n_server_predict]) if context.server_api_key is not None: server_args.extend(['--api-key', context.server_api_key]) - if 'DEBUG' in os.environ and os.environ['DEBUG'] == 'ON': + if context.debug: server_args.append('--verbose') print(f"starting server with: {context.server_path}", *server_args) context.server_process = subprocess.Popen( From 482eb30f89bfe12eb0f276c19b060a99c1369c50 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 11:13:14 +0100 Subject: [PATCH 93/98] server: tests - README.md add build instruction and notice on @bug and @wrong_usage. --- examples/server/tests/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index fb59cf331797e..521ecf4843ee9 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -13,6 +13,13 @@ Note: If the host architecture inference speed is faster than GitHub runners one ### Run tests 1. Build the server +```shell +cd ../../.. +mkdir build +cd build +cmake ../ +cmake --build . --target server +``` 2. download required models: 1. `../../../scripts/hf.sh --repo ggml-org/models --file tinyllamas/stories260K.gguf` 3. Start the test: `./tests.sh` @@ -20,7 +27,7 @@ Note: If the host architecture inference speed is faster than GitHub runners one It's possible to override some scenario steps values with environment variables: - `PORT` -> `context.server_port` to set the listening port of the server during scenario, default: `8080` - `LLAMA_SERVER_BIN_PATH` -> to change the server binary path, default: `../../../build/bin/server` - - `DEBUG` -> "ON" to enable server verbose mode `--verbose` + - `DEBUG` -> "ON" to enable steps and server verbose mode `--verbose` ### Run @bug, @wip or @wrong_usage annotated scenario @@ -31,3 +38,5 @@ Feature or Scenario must be annotated with `@llama.cpp` to be included in the de To run a scenario annotated with `@bug`, start: `DEBUG=ON ./tests.sh --no-skipped --tags bug` + +After changing logic in `steps.py`, ensure that `@bug` and `@wrong_usage` scenario are updated. From 60781f0a2bd8ec3aa8444b453cca978de27e24ef Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 11:13:31 +0100 Subject: [PATCH 94/98] server: tests - add explanation about KV Cache. --- examples/server/tests/features/server.feature | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/server/tests/features/server.feature b/examples/server/tests/features/server.feature index a98d92c09ab45..fedcfe5aef1b3 100644 --- a/examples/server/tests/features/server.feature +++ b/examples/server/tests/features/server.feature @@ -6,6 +6,9 @@ Feature: llama.cpp server And a model file stories260K.gguf And a model alias tinyllama-2 And 42 as server seed + # KV Cache corresponds to the total amount of tokens + # that can be stored across all independent sequences: #4130 + # see --ctx-size and #5568 And 32 KV cache size And 1 slots And embeddings extraction From a779a4bf9cc38da84c52e2b3246900d3e4632c36 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 11:13:43 +0100 Subject: [PATCH 95/98] server: tests - print only in case of DEBUG --- examples/server/tests/features/steps/steps.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/examples/server/tests/features/steps/steps.py b/examples/server/tests/features/steps/steps.py index bbe7c3f6cae25..50f2b641e764e 100644 --- a/examples/server/tests/features/steps/steps.py +++ b/examples/server/tests/features/steps/steps.py @@ -146,12 +146,14 @@ async def step_request_completion(context, api_error): expect_api_error = api_error == 'raised' completion = await request_completion(context.prompts.pop(), context.base_url, + debug=context.debug, n_predict=context.n_predict, server_seed=context.server_seed, expect_api_error=expect_api_error, user_api_key=context.user_api_key) context.tasks_result.append(completion) - print(f"Completion response: {completion}") + if context.debug: + print(f"Completion response: {completion}") if expect_api_error: assert completion == 401, f"completion must be an 401 status code: {completion}" @@ -214,7 +216,8 @@ def step_server_api_key(context, server_api_key): @step(u'an OAI compatible chat completions request with {api_error} api error') @async_run_until_complete async def step_oai_chat_completions(context, api_error): - print(f"Submitting OAI compatible completions request...") + if context.debug: + print(f"Submitting OAI compatible completions request...") expect_api_error = api_error == 'raised' completion = await oai_chat_completions(context.prompts.pop(), context.system_prompt, @@ -236,11 +239,13 @@ async def step_oai_chat_completions(context, api_error): expect_api_error=expect_api_error) context.tasks_result.append(completion) - print(f"Completion response: {completion}") + if context.debug: + print(f"Completion response: {completion}") if expect_api_error: assert completion == 401, f"completion must be an 401 status code: {completion}" - print(f"Completion response: {completion}") + if context.debug: + print(f"Completion response: {completion}") @step(u'a prompt') @@ -260,6 +265,7 @@ async def step_concurrent_completion_requests(context): request_completion, # prompt is inserted automatically context.base_url, + debug=context.debug, n_predict=context.n_predict if hasattr(context, 'n_predict') else None, server_seed=context.server_seed if hasattr(context, 'server_seed') else None, user_api_key=context.user_api_key if hasattr(context, @@ -397,7 +403,8 @@ def step_check_options_header_value(context, cors_header, cors_header_value): async def concurrent_completion_requests(context, f_completion, *args, **kwargs): n_prompts = len(context.prompts) - print(f"starting {n_prompts} concurrent completion requests...") + if context.debug: + print(f"starting {n_prompts} concurrent completion requests...") assert n_prompts > 0 for prompt_no in range(n_prompts): shifted_args = [context.prompts.pop(), *args] @@ -407,17 +414,20 @@ async def concurrent_completion_requests(context, f_completion, *args, **kwargs) async def request_completion(prompt, base_url, + debug=False, n_predict=None, server_seed=None, expect_api_error=None, user_api_key=None): - print(f"Sending completion request: {prompt}") + if debug: + print(f"Sending completion request: {prompt}") origin = "my.super.domain" headers = { 'Origin': origin } if user_api_key is not None: - print(f"Set user_api_key: {user_api_key}") + if debug: + print(f"Set user_api_key: {user_api_key}") headers['Authorization'] = f'Bearer {user_api_key}' async with aiohttp.ClientSession() as session: @@ -440,13 +450,15 @@ async def oai_chat_completions(user_prompt, system_prompt, base_url, async_client, + debug=False, model=None, n_predict=None, enable_streaming=None, server_seed=None, user_api_key=None, expect_api_error=None): - print(f"Sending OAI Chat completions request: {user_prompt}") + if debug: + print(f"Sending OAI Chat completions request: {user_prompt}") # openai client always expects an api key user_api_key = user_api_key if user_api_key is not None else 'nope' seed = server_seed if server_seed is not None else 42 @@ -548,7 +560,8 @@ async def oai_chat_completions(user_prompt, 'predicted_n': chat_completion.usage.completion_tokens } } - print("OAI response formatted to llama.cpp:", completion_response) + if debug: + print("OAI response formatted to llama.cpp:", completion_response) return completion_response @@ -579,7 +592,8 @@ def assert_n_tokens_predicted(completion_response, expected_predicted_n=None, re async def gather_tasks_results(context): n_tasks = len(context.concurrent_tasks) - print(f"Waiting for all {n_tasks} tasks results...") + if context.debug: + print(f"Waiting for all {n_tasks} tasks results...") for task_no in range(n_tasks): context.tasks_result.append(await context.concurrent_tasks.pop()) n_completions = len(context.tasks_result) From a2a928c5a93561e4cf744f9385b8887eb4038131 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 11:25:50 +0100 Subject: [PATCH 96/98] server: add link to tests in the README.md --- examples/server/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/server/README.md b/examples/server/README.md index 4b6cd8326efa8..0c43ac4c97cba 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -98,6 +98,12 @@ curl --request POST \ --data '{"prompt": "Building a website can be done in 10 simple steps:","n_predict": 128}' ``` +## Advanced testing + +We implemented a [server test framework](./tests/README.md) using human-readable scenario. + +*Before submitting an issue, please try to reproduce it with this format.* + ## Node JS Test You need to have [Node.js](https://nodejs.org/en) installed. From 5ed445283c7dcbf9f103a3210459a1c062317950 Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 11:26:08 +0100 Subject: [PATCH 97/98] server: tests: improved README.md --- examples/server/tests/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/server/tests/README.md b/examples/server/tests/README.md index 521ecf4843ee9..e44c5c286601f 100644 --- a/examples/server/tests/README.md +++ b/examples/server/tests/README.md @@ -1,6 +1,10 @@ # Server tests -Python based server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) and [behave](https://behave.readthedocs.io/en/latest/). +Python based server tests scenario using [BDD](https://en.wikipedia.org/wiki/Behavior-driven_development) and [behave](https://behave.readthedocs.io/en/latest/): + * [issues.feature](./features/issues.feature) Pending issues scenario + * [parallel.feature](./features/parallel.feature) Scenario involving multi slots and concurrent requests + * [security.feature](./features/security.feature) Security, CORS and API Key + * [server.feature](./features/server.feature) Server base scenario: completion, embedding, tokenization, etc... Tests target GitHub workflows job runners with 4 vCPU. From 99163c83bd23d155fb605a0f870f71cbc134389b Mon Sep 17 00:00:00 2001 From: Pierrick HYMBERT Date: Sat, 24 Feb 2024 11:26:39 +0100 Subject: [PATCH 98/98] github issue template: add link to the tests server framework --- .github/ISSUE_TEMPLATE/bug.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index ce69e6395daae..49812832ca542 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -7,3 +7,5 @@ assignees: '' --- Please include information about your system, the steps to reproduce the bug, and the version of llama.cpp that you are using. If possible, please provide a minimal code example that reproduces the bug. + +If the bug concerns the server, please try to reproduce it first using the [server test scenario framework](https://github.com/ggerganov/llama.cpp/tree/master/examples/server/tests).