Skip to content

Commit 3848f83

Browse files
committed
Implement TCP server mode.
This new mode works by first loading the model then listening for TCP connections on a port. When a connection is received, arguments will be parsed using a simple protocol: - First the number of arguments will be read followed by a newline character. - Then each argument will be read, separated by the 0 byte. - With this we build an argument vector, similar to what is passed to the program entry point. We pass this to gpt_params_parse. Finally `llama_main` will be executed with the input/output streams connected to the socket. Signed-off-by: Thiago Padilha <thiago@padilha.cc>
1 parent 60d18c3 commit 3848f83

9 files changed

+298
-4
lines changed

CMakeLists.txt

+9-2
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,17 @@ endif()
106106
# set(LLAMA_EXTRA_FLAGS ${LLAMA_EXTRA_FLAGS} -DGGML_PERF)
107107
# endif()
108108

109-
add_executable(llama
109+
set(LLAMA_SRC
110110
main.cpp
111111
utils.cpp
112-
llama.cpp
112+
llama.cpp)
113+
114+
if(NOT WIN32)
115+
set(LLAMA_SRC ${LLAMA_SRC} tcp_server.cpp)
116+
endif()
117+
118+
add_executable(llama
119+
${LLAMA_SRC}
113120
utils.h)
114121

115122
add_executable(quantize

Makefile

+5-2
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,14 @@ utils.o: utils.cpp utils.h
191191
llama.o: llama.cpp llama.h
192192
$(CXX) $(CXXFLAGS) -c llama.cpp -o llama.o
193193

194+
tcp_server.o: tcp_server.cpp tcp_server.h
195+
$(CXX) $(CXXFLAGS) -c tcp_server.cpp -o tcp_server.o
196+
194197
clean:
195198
rm -f *.o main quantize
196199

197-
main: main.cpp ggml.o utils.o llama.o
198-
$(CXX) $(CXXFLAGS) main.cpp ggml.o utils.o llama.o -o main $(LDFLAGS)
200+
main: main.cpp ggml.o utils.o llama.o tcp_server.o
201+
$(CXX) $(CXXFLAGS) main.cpp ggml.o utils.o llama.o tcp_server.o -o main $(LDFLAGS)
199202
./main -h
200203

201204
quantize: quantize.cpp ggml.o utils.o

chat_tcp_client.sh

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
3+
PORT=${PORT:-8080}
4+
PROMPT="${PROMPT:-"Transcript of a dialog, where the User interacts with an Assistant named Bob. Bob is helpful, kind, honest, good at writing, and never fails to answer the User's requests immediately and with precision.
5+
6+
User: Hello, Bob.
7+
Bob: Hello. How may I help you today?
8+
User: Please tell me the largest city in Europe.
9+
Bob: Sure. The largest city in Europe is Moscow, the capital of Russia.
10+
User:"}"
11+
RPROMPT="${RPROMPT:-"User:"}"
12+
N_PREDICT="${N_PREDICT:-"4096"}"
13+
REPEAT_PENALTY="${REPEAT_PENALTY:-"1.0"}"
14+
15+
# Open connection to the chat server
16+
exec 3<>/dev/tcp/127.0.0.1/${PORT}
17+
18+
# Pass the arguments. The protocol is really simple:
19+
# 1. Pass the number of arguments followed by a linefeed
20+
# 2. Pass the arguments, with each being followed by "0"
21+
(
22+
echo -en "10\n"
23+
echo -en "-n\x00"
24+
echo -en "$N_PREDICT\x00"
25+
echo -en "--repeat_penalty\x00"
26+
echo -en "$REPEAT_PENALTY\x00"
27+
echo -en "--color\x00"
28+
echo -en "-i\x00"
29+
echo -en "-r\x00"
30+
echo -en "$RPROMPT\x00"
31+
echo -en "-p\x00"
32+
echo -en "$PROMPT\x00"
33+
) >&3
34+
35+
trap exit TERM
36+
37+
# When we have passed the arguments, start printing socket data to the screen.
38+
# This is done in a background job because we also want to send data when
39+
# running in interactive mode.
40+
cat <&3 && echo "(disconnected, press \"enter\" twice to exit)" &
41+
cat >&3
42+
wait

chat_tcp_server.sh

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
3+
PORT=${PORT:-8080}
4+
MODEL=${MODEL:-models/7B/ggml-model-q4_0.bin}
5+
6+
./main -l ${PORT} -m $MODEL

main.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "ggml.h"
22
#include "utils.h"
33
#include "llama.h"
4+
#include "tcp_server.h"
45

56
const char * llama_print_system_info(void) {
67
static std::string s;
@@ -63,5 +64,11 @@ int main(int argc, char ** argv) {
6364
params.n_threads, std::thread::hardware_concurrency(), llama_print_system_info());
6465
}
6566

67+
#ifndef _WIN32
68+
if (params.listen_port != "") {
69+
return listen_tcp(params, vocab, model, t_main_start_us, t_load_us);
70+
}
71+
#endif
72+
6673
return llama_main(params, vocab, model, t_main_start_us, t_load_us, stdin, stdout, stderr);
6774
}

tcp_server.cpp

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#include "tcp_server.h"
2+
3+
#include <stdarg.h>
4+
#include <stdio.h>
5+
#include <stdlib.h>
6+
#include <stdbool.h>
7+
#include <string.h>
8+
#include <errno.h>
9+
10+
#include <signal.h>
11+
#include <unistd.h>
12+
#include <sys/wait.h>
13+
14+
#include <sys/types.h>
15+
#include <sys/socket.h>
16+
#include <netdb.h>
17+
18+
void die(const char *msg, ...)
19+
{
20+
va_list ap;
21+
22+
va_start(ap, msg);
23+
vfprintf(stderr, msg, ap);
24+
va_end(ap);
25+
fputc('\n', stderr);
26+
exit(1);
27+
}
28+
29+
static char *read_argument(uint8_t **param_buf, size_t *param_buf_size, FILE *instream) {
30+
bool done = false;
31+
uint8_t *buf = *param_buf;
32+
size_t bufsize = *param_buf_size;
33+
size_t bufpos = 0;
34+
while (!done) {
35+
if (bufpos == bufsize) {
36+
bufsize += 1024;
37+
buf = (uint8_t *)realloc(buf, bufsize);
38+
if (!buf) {
39+
die("failed to allocate memory");
40+
}
41+
}
42+
43+
int c = fgetc(instream);
44+
if (c == EOF) {
45+
die("unexpected EOF client socket");
46+
}
47+
buf[bufpos++] = (uint8_t)c;
48+
if (c == 0) {
49+
// done reading argument
50+
break;
51+
}
52+
}
53+
*param_buf = buf;
54+
*param_buf_size = bufsize;
55+
return strdup((char *)buf);
56+
}
57+
58+
static int read_arguments(int argc, char **argv, FILE *instream) {
59+
int i = 1;
60+
size_t param_buf_size = 0;
61+
uint8_t *param_buf = nullptr;
62+
63+
for (i = 1; i < argc; i++) {
64+
argv[i] = read_argument(&param_buf, &param_buf_size, instream);
65+
}
66+
67+
free(param_buf);
68+
return i;
69+
}
70+
71+
static int serve_model(
72+
gpt_params params,
73+
gpt_vocab vocab,
74+
llama_model model,
75+
int64_t t_load_us,
76+
int64_t t_main_start_us,
77+
int sock_fd)
78+
{
79+
char *response_data;
80+
int argc;
81+
char **argv;
82+
FILE *instream = fdopen(sock_fd, "r");
83+
FILE *outstream = fdopen(sock_fd, "w");
84+
setvbuf(instream, NULL, _IONBF, 0);
85+
86+
// start by reading the parameter count
87+
if (fscanf(instream, "%d\n", &argc) != 1) {
88+
fprintf(outstream, "Error: First line must be character count\n");
89+
fflush(outstream);
90+
return 1;
91+
}
92+
93+
argc += 1; // add one extra argument to emulate the program command line
94+
argv = (char **)malloc(argc * sizeof *argv);
95+
argv[0] = nullptr;
96+
if (read_arguments(argc, argv, instream) != argc) {
97+
fprintf(outstream, "Error: Failed to read arguments\n");
98+
fflush(outstream);
99+
}
100+
101+
if (gpt_params_parse(argc, argv, params) == false) {
102+
fprintf(outstream, "Error: Failed to parse parameters\n");
103+
fflush(outstream);
104+
return 1;
105+
}
106+
107+
for (int i = 1; i < argc; i++) {
108+
free(argv[i]);
109+
}
110+
free(argv);
111+
112+
return llama_main(params, vocab, model, t_load_us, t_main_start_us, instream, outstream, outstream);
113+
}
114+
115+
int listen_tcp(
116+
gpt_params params,
117+
gpt_vocab vocab,
118+
llama_model model,
119+
int64_t t_main_start_us,
120+
int64_t t_load_us) {
121+
int listen_fd;
122+
int status;
123+
pid_t child;
124+
struct addrinfo hints;
125+
struct addrinfo *servinfo, *p;
126+
int yes = 1;
127+
128+
memset(&hints, 0, sizeof hints);
129+
hints.ai_family = AF_INET;
130+
hints.ai_socktype = SOCK_STREAM;
131+
hints.ai_flags = AI_PASSIVE;
132+
133+
// This should only ever listen on a loopback address. Access from outside
134+
// should be proxied via nginx or similar software
135+
status = getaddrinfo("127.0.0.1", params.listen_port.c_str(), &hints, &servinfo);
136+
if (status) {
137+
die("getaddrinfo error: %s", gai_strerror(status));
138+
}
139+
140+
// bind to the first addrinfo we can from the getaddrinfo results
141+
for (p = servinfo; p != NULL; p = p->ai_next) {
142+
listen_fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
143+
if (listen_fd == -1) {
144+
perror("server: socket");
145+
continue;
146+
}
147+
148+
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes)) {
149+
die("setsockopt error: %s", params.listen_port.c_str(), strerror(errno));
150+
}
151+
152+
if (bind(listen_fd, p->ai_addr, p->ai_addrlen) == 0) {
153+
break;
154+
}
155+
156+
close(listen_fd);
157+
perror("server: bind");
158+
}
159+
160+
freeaddrinfo(servinfo);
161+
162+
if (p == NULL) {
163+
die("failed to bind: %s", strerror(errno));
164+
}
165+
166+
if (listen(listen_fd, 20)) {
167+
die("listen error: %s", strerror(errno));
168+
}
169+
// Don't track child processes, so ignore SIGCHLD to prevent zombies
170+
signal(SIGCHLD, SIG_IGN);
171+
172+
for (;;) {
173+
struct sockaddr_in client_addr = {0};
174+
socklen_t client_addr_len = 0;
175+
176+
int sock_fd = accept(listen_fd,
177+
(struct sockaddr *)&client_addr,
178+
&client_addr_len);
179+
if (sock_fd < 0) {
180+
fprintf(stderr, "accept error: %s\n", strerror(errno));
181+
break;
182+
}
183+
184+
child = fork();
185+
if (child == 0) {
186+
// close the listen_fd since we won't use it in the child
187+
close(listen_fd);
188+
int ret = serve_model(params, vocab, model, t_main_start_us, t_load_us, sock_fd);
189+
close(sock_fd);
190+
return ret;
191+
} else {
192+
// close the client since we won't use it in the server
193+
close(sock_fd);
194+
sock_fd = 0;
195+
}
196+
}
197+
close(listen_fd);
198+
199+
// ignore SIGTERM since we'll send it to the group
200+
signal(SIGTERM, SIG_IGN);
201+
// tell children to exit
202+
kill(0, SIGTERM);
203+
// wait for children to terminate
204+
wait(&status);
205+
return 0;
206+
}

tcp_server.h

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#pragma once
2+
3+
#include "utils.h"
4+
#include "llama.h"
5+
6+
int listen_tcp(
7+
gpt_params params,
8+
gpt_vocab vocab,
9+
llama_model model,
10+
int64_t t_main_start_us,
11+
int64_t t_load_us);

utils.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ bool gpt_params_parse(int argc, char ** argv, gpt_params & params) {
7373
params.antiprompt.push_back(argv[++i]);
7474
} else if (arg == "--ignore-eos") {
7575
params.ignore_eos = true;
76+
#ifndef _WIN32
77+
} else if (arg == "-l" || arg == "--listen") {
78+
params.listen_port = argv[++i];
79+
#endif
7680
} else if (arg == "-h" || arg == "--help") {
7781
gpt_print_usage(argc, argv, params);
7882
exit(0);
@@ -118,6 +122,10 @@ void gpt_print_usage(int /*argc*/, char ** argv, const gpt_params & params) {
118122
fprintf(stderr, " -b N, --batch_size N batch size for prompt processing (default: %d)\n", params.n_batch);
119123
fprintf(stderr, " -m FNAME, --model FNAME\n");
120124
fprintf(stderr, " model path (default: %s)\n", params.model.c_str());
125+
#ifndef _WIN32
126+
fprintf(stderr, " -l PORT, --listen PORT\n");
127+
fprintf(stderr, " Run in TCP mode, listening on PORT\n");
128+
#endif
121129
fprintf(stderr, "\n");
122130
}
123131

utils.h

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ struct gpt_params {
4040
std::vector<std::string> antiprompt; // string upon seeing which more user input is prompted
4141
bool instruct = false; // instruction mode (used for Alpaca models)
4242
bool ignore_eos = false; // do not stop generating after eos
43+
44+
#ifndef _WIN32
45+
std::string listen_port = ""; // TCP port for when running in server mode
46+
#endif
4347
};
4448

4549
bool gpt_params_parse(int argc, char ** argv, gpt_params & params);

0 commit comments

Comments
 (0)