diff --git a/wamr-wasi-extensions/samples/nn-cli/CMakeLists.txt b/wamr-wasi-extensions/samples/nn-cli/CMakeLists.txt new file mode 100644 index 000000000..df90b5ba4 --- /dev/null +++ b/wamr-wasi-extensions/samples/nn-cli/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (C) 2025 Midokura Japan KK. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.14) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED YES) + +project(nn-cli LANGUAGES C) +add_executable(nn-cli main.c fileio.c map.c) +find_package(wamr-wasi-nn REQUIRED) +target_link_libraries(nn-cli wamr-wasi-nn) diff --git a/wamr-wasi-extensions/samples/nn-cli/fileio.c b/wamr-wasi-extensions/samples/nn-cli/fileio.c new file mode 100644 index 000000000..5d8163ba4 --- /dev/null +++ b/wamr-wasi-extensions/samples/nn-cli/fileio.c @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 Midokura Japan KK. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* + * modified copy-and-paste from: + * https://github.com/yamt/toywasm/blob/0eaad8cacd0cc7692946ff19b25994f106113be8/lib/fileio.c + */ + +#include + +#include +#include +#include +#include +#include + +#include "fileio.h" + +int +map_file(const char *path, void **pp, size_t *sizep) +{ + void *p; + size_t size; + ssize_t ssz; + int fd; + int ret; + + fd = open(path, O_RDONLY); + if (fd == -1) { + ret = errno; + assert(ret != 0); + return ret; + } + struct stat st; + ret = fstat(fd, &st); + if (ret == -1) { + ret = errno; + assert(ret != 0); + close(fd); + return ret; + } + size = st.st_size; + if (size > 0) { + p = malloc(size); + } + else { + /* Avoid a confusing error */ + p = malloc(1); + } + if (p == NULL) { + close(fd); + return ENOMEM; + } + ssz = read(fd, p, size); + if (ssz != size) { + ret = errno; + assert(ret != 0); + close(fd); + return ret; + } + close(fd); + *pp = p; + *sizep = size; + return 0; +} + +void +unmap_file(void *p, size_t sz) +{ + free(p); +} diff --git a/wamr-wasi-extensions/samples/nn-cli/fileio.h b/wamr-wasi-extensions/samples/nn-cli/fileio.h new file mode 100644 index 000000000..e268222bc --- /dev/null +++ b/wamr-wasi-extensions/samples/nn-cli/fileio.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2025 Midokura Japan KK. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* + * modified copy-and-paste from: + * https://github.com/yamt/toywasm/blob/0eaad8cacd0cc7692946ff19b25994f106113be8/lib/fileio.h + */ + +int +map_file(const char *filename, void **pp, size_t *szp); +void +unmap_file(void *p, size_t sz); diff --git a/wamr-wasi-extensions/samples/nn-cli/main.c b/wamr-wasi-extensions/samples/nn-cli/main.c new file mode 100644 index 000000000..894ac455e --- /dev/null +++ b/wamr-wasi-extensions/samples/nn-cli/main.c @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2025 Midokura Japan KK. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "fileio.h" +#include "map.h" + +static struct map graphs; +static struct map contexts; + +static void +load_graph(char *options) +{ + int target = wasi_ephemeral_nn_target_cpu; + int encoding = wasi_ephemeral_nn_encoding_openvino; + const char *id = "default"; + wasi_ephemeral_nn_graph_builder *builders = NULL; + size_t nbuilders = 0; + enum { + opt_id, + opt_file, + opt_encoding, + opt_target, + }; + static char *const keylistp[] = { + [opt_id] = "id", + [opt_file] = "file", + [opt_encoding] = "encoding", + [opt_target] = "target", + NULL, + }; + while (*options) { + extern char *suboptarg; + char *value; + const char *saved = options; + switch (getsubopt(&options, keylistp, &value)) { + case opt_id: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + id = value; + break; + case opt_file: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + builders = + realloc(builders, (nbuilders + 1) * sizeof(*builders)); + if (builders == NULL) { + exit(1); + } + wasi_ephemeral_nn_graph_builder *b = &builders[nbuilders++]; + int ret = map_file(value, (void *)&b->buf, (void *)&b->size); + if (ret != 0) { + fprintf(stderr, "map_file \"%s\" failed: %s\n", value, + strerror(ret)); + exit(1); + } + break; + case opt_encoding: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + encoding = atoi(value); + break; + case opt_target: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + target = atoi(value); + break; + case -1: + fprintf(stderr, "unknown subopt %s\n", saved); + exit(2); + } + } + + wasi_ephemeral_nn_error nnret; + wasi_ephemeral_nn_graph g; + nnret = wasi_ephemeral_nn_load(builders, nbuilders, encoding, target, &g); + size_t i; + for (i = 0; i < nbuilders; i++) { + wasi_ephemeral_nn_graph_builder *b = &builders[i]; + unmap_file(b->buf, b->size); + } + if (nnret != wasi_ephemeral_nn_error_success) { + fprintf(stderr, "load failed with %d\n", (int)nnret); + exit(1); + } + map_set(&graphs, id, g); +} + +static void +init_execution_context(char *options) +{ + const char *id = "default"; + const char *graph_id = "default"; + enum { + opt_id, + opt_graph_id, + }; + static char *const keylistp[] = { + [opt_id] = "id", + [opt_graph_id] = "graph-id", + NULL, + }; + while (*options) { + extern char *suboptarg; + char *value; + const char *saved = options; + switch (getsubopt(&options, keylistp, &value)) { + case opt_id: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + id = value; + break; + case opt_graph_id: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + graph_id = value; + break; + case -1: + fprintf(stderr, "unknown subopt %s\n", saved); + exit(2); + } + } + + wasi_ephemeral_nn_graph g = map_get(&graphs, graph_id); + wasi_ephemeral_nn_graph_execution_context c; + wasi_ephemeral_nn_error nnret; + nnret = wasi_ephemeral_nn_init_execution_context(g, &c); + if (nnret != wasi_ephemeral_nn_error_success) { + fprintf(stderr, "init_execution_context failed with %d\n", (int)nnret); + exit(1); + } + map_set(&contexts, id, c); +} + +static void +set_input(char *options) +{ + int ret; + const char *context_id = "default"; + uint32_t idx = 0; + wasi_ephemeral_nn_tensor tensor = { + .dimensions = { .buf = NULL, .size = 0, }, + .type = wasi_ephemeral_nn_type_fp32, + .data = NULL, + }; + void *buf = NULL; + size_t sz = 0; + enum { + opt_context_id, + opt_dim, + opt_type, + opt_idx, + opt_file, + }; + static char *const keylistp[] = { + [opt_context_id] = "context-id", + [opt_dim] = "dim", + [opt_type] = "type", + [opt_idx] = "idx", + [opt_file] = "file", + NULL, + }; + while (*options) { + extern char *suboptarg; + char *value; + const char *saved = options; + switch (getsubopt(&options, keylistp, &value)) { + case opt_context_id: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + context_id = value; + break; + case opt_dim: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + wasi_ephemeral_nn_tensor_dimensions *dims = &tensor.dimensions; + + dims->buf = + realloc(dims->buf, (dims->size + 1) * sizeof(*dims->buf)); + if (dims->buf == NULL) { + exit(1); + } + dims->buf[dims->size++] = atoi(value); + break; + case opt_type: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + tensor.type = atoi(value); + break; + case opt_file: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + if (buf != NULL) { + fprintf(stderr, "duplicated tensor data\n"); + exit(2); + } + ret = map_file(value, &buf, &sz); + if (ret != 0) { + fprintf(stderr, "map_file \"%s\" failed: %s\n", value, + strerror(ret)); + exit(1); + } + break; + case opt_idx: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + idx = atoi(value); + break; + case -1: + fprintf(stderr, "unknown subopt %s\n", saved); + exit(2); + } + } + + if (tensor.dimensions.size == 0) { + fprintf(stderr, "no dimension is given\n"); + exit(2); + } + if (buf == NULL) { + fprintf(stderr, "no tensor is given\n"); + exit(2); + } + + /* + * REVISIT: we can check the tensor size against type/dimensions + * and warn the user if unexpected. + */ + + wasi_ephemeral_nn_error nnret; + wasi_ephemeral_nn_graph_execution_context c = + map_get(&contexts, context_id); + tensor.data = buf; + nnret = wasi_ephemeral_nn_set_input(c, idx, &tensor); + unmap_file(buf, sz); + if (nnret != wasi_ephemeral_nn_error_success) { + fprintf(stderr, "set_input failed with %d\n", (int)nnret); + exit(1); + } +} + +static void +compute(char *options) +{ + const char *context_id = "default"; + enum { + opt_context_id, + }; + static char *const keylistp[] = { + [opt_context_id] = "context-id", + NULL, + }; + while (*options) { + extern char *suboptarg; + char *value; + const char *saved = options; + switch (getsubopt(&options, keylistp, &value)) { + case opt_context_id: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + context_id = value; + break; + case -1: + fprintf(stderr, "unknown subopt %s\n", saved); + exit(2); + } + } + + wasi_ephemeral_nn_graph_execution_context c = + map_get(&contexts, context_id); + wasi_ephemeral_nn_error nnret; + nnret = wasi_ephemeral_nn_compute(c); + if (nnret != wasi_ephemeral_nn_error_success) { + fprintf(stderr, "compute failed with %d\n", (int)nnret); + exit(1); + } +} + +static void +get_output(char *options) +{ + int ret; + const char *outfile = NULL; + const char *context_id = "default"; + uint32_t idx = 0; + enum { + opt_context_id, + opt_idx, + opt_file, + }; + static char *const keylistp[] = { + [opt_context_id] = "context-id", + [opt_idx] = "idx", + [opt_file] = "file", + NULL, + }; + while (*options) { + extern char *suboptarg; + char *value; + const char *saved = options; + switch (getsubopt(&options, keylistp, &value)) { + case opt_context_id: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + context_id = value; + break; + case opt_file: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + outfile = value; + break; + case opt_idx: + if (value == NULL) { + fprintf(stderr, "no value for %s\n", saved); + exit(2); + } + idx = atoi(value); + break; + case -1: + fprintf(stderr, "unknown subopt %s\n", saved); + exit(2); + } + } + + int outfd = -1; + if (outfile != NULL) { + outfd = open(outfile, O_CREAT | O_TRUNC | O_WRONLY); + if (outfd == -1) { + fprintf(stderr, "failed to open output file \"%s\": %s\n", outfile, + strerror(errno)); + exit(1); + } + } + + wasi_ephemeral_nn_error nnret; + wasi_ephemeral_nn_graph_execution_context c = + map_get(&contexts, context_id); + void *resultbuf = NULL; + size_t resultbufsz = 256; + uint32_t resultsz; +retry: + resultbuf = realloc(resultbuf, resultbufsz); + if (resultbuf == NULL) { + exit(1); + } + nnret = + wasi_ephemeral_nn_get_output(c, 0, resultbuf, resultbufsz, &resultsz); + if (nnret == wasi_ephemeral_nn_error_too_large) { + resultbufsz *= 2; + goto retry; + } + if (nnret != wasi_ephemeral_nn_error_success) { + fprintf(stderr, "get_output failed with %d\n", (int)nnret); + exit(1); + } + if (outfd != -1) { + ssize_t written = write(outfd, resultbuf, resultsz); + if (written == -1) { + fprintf(stderr, "failed to write: %s\n", strerror(errno)); + exit(1); + } + if (written == -1) { + fprintf(stderr, "unexpetecd write length %zu (expected %zu)\n", + written, (size_t)resultsz); + exit(1); + } + ret = close(outfd); + if (ret != 0) { + fprintf(stderr, "failed to close: %s\n", strerror(errno)); + exit(1); + } + } + else { + fprintf(stderr, "WARNING: discarding %zu bytes output\n", + (size_t)resultsz); + } +} + +enum longopt { + opt_load_graph = 0x100, + opt_init_execution_context, + opt_set_input, + opt_compute, + opt_get_output, +}; + +static const struct option longopts[] = { + { + "load-graph", + required_argument, + NULL, + opt_load_graph, + }, + { + "init-execution-context", + optional_argument, + NULL, + opt_init_execution_context, + }, + { + "set-input", + required_argument, + NULL, + opt_set_input, + }, + { + "compute", + optional_argument, + NULL, + opt_compute, + }, + { + "get-output", + optional_argument, + NULL, + opt_get_output, + }, + { + NULL, + 0, + NULL, + 0, + }, +}; + +int +main(int argc, char **argv) +{ + extern char *optarg; + int ch; + int longidx; + while ((ch = getopt_long(argc, argv, "", longopts, &longidx)) != -1) { + switch (ch) { + case opt_load_graph: + load_graph(optarg); + break; + case opt_init_execution_context: + init_execution_context(optarg ? optarg : ""); + break; + case opt_set_input: + set_input(optarg); + break; + case opt_compute: + compute(optarg ? optarg : ""); + break; + case opt_get_output: + get_output(optarg ? optarg : ""); + break; + default: + exit(2); + } + } + exit(0); +} diff --git a/wamr-wasi-extensions/samples/nn-cli/map.c b/wamr-wasi-extensions/samples/nn-cli/map.c new file mode 100644 index 000000000..3ed817242 --- /dev/null +++ b/wamr-wasi-extensions/samples/nn-cli/map.c @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 Midokura Japan KK. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include +#include +#include +#include +#include + +#include "map.h" + +static uintmax_t * +map_find_slot(struct map *m, const char *name) +{ + size_t i; + for (i = 0; i < m->nentries; i++) { + if (!strcmp(m->entries[i].k, name)) { + return &m->entries[i].v; + } + } + return NULL; +} + +static void +map_append(struct map *m, const char *k, uintmax_t v) +{ + m->entries = realloc(m->entries, (m->nentries + 1) * sizeof(*m->entries)); + if (m->entries == NULL) { + exit(1); + } + struct map_entry *e = &m->entries[m->nentries++]; + e->k = k; + e->v = v; +} + +void +map_set(struct map *m, const char *k, uintmax_t v) +{ + uintmax_t *p = map_find_slot(m, k); + if (p != NULL) { + fprintf(stderr, "duplicated id \"%s\"\n", k); + exit(1); + } + map_append(m, k, v); +} + +uintmax_t +map_get(struct map *m, const char *k) +{ + uintmax_t *p = map_find_slot(m, k); + if (p == NULL) { + fprintf(stderr, "id \"%s\" not found\n", k); + exit(1); + } + return *p; +} diff --git a/wamr-wasi-extensions/samples/nn-cli/map.h b/wamr-wasi-extensions/samples/nn-cli/map.h new file mode 100644 index 000000000..0059293c8 --- /dev/null +++ b/wamr-wasi-extensions/samples/nn-cli/map.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2025 Midokura Japan KK. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#include + +struct map { + struct map_entry { + const char *k; + uintmax_t v; + } * entries; + size_t nentries; +}; + +void +map_set(struct map *m, const char *k, uintmax_t v); +uintmax_t +map_get(struct map *m, const char *k); diff --git a/wamr-wasi-extensions/test.sh b/wamr-wasi-extensions/test.sh index bf3c45b14..e485cf48c 100755 --- a/wamr-wasi-extensions/test.sh +++ b/wamr-wasi-extensions/test.sh @@ -20,6 +20,12 @@ cmake -B build-app-nn \ samples/nn cmake --build build-app-nn +cmake -B build-app-nn-cli \ +-DCMAKE_TOOLCHAIN_FILE=${WASI_SDK}/share/cmake/wasi-sdk.cmake \ +-DCMAKE_PREFIX_PATH=${PREFIX} \ +samples/nn-cli +cmake --build build-app-nn-cli + cmake -B build-app-socket-nslookup \ -DCMAKE_TOOLCHAIN_FILE=${WASI_SDK}/share/cmake/wasi-sdk-pthread.cmake \ -DCMAKE_PREFIX_PATH=${PREFIX} \