diff --git a/.dockerignore b/.dockerignore index 912e59d..0f00c40 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,16 +2,23 @@ .github .gitignore .dockerignore -Dockerfile +README.md +bench docs -img tests -# Makefile artifacts +# The image is built from the repository root, so that the Makefile, src/ and +# kernels/ are in the context. The Dockerfile itself is not needed inside it, +# but docker/entrypoint.sh is, so the directory cannot be excluded wholesale. +docker/Dockerfile + +# Makefile artifacts, the image runs its own build +build +bin *.o *.so *.x64 *.exe cache-opencl.* -bin __pycache__/ +.venv diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2270acb..63908b5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -27,6 +27,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . + file: docker/Dockerfile load: true tags: profanity2:ci cache-from: type=gha @@ -65,6 +66,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . + file: docker/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 8f7ee92..0a95e98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # Makefile artifacts +build/ +bin/ *.o *.so *.x64 +*.exe cache-opencl.* -bin __pycache__/ .venv diff --git a/Makefile b/Makefile index b2dbde1..7871f56 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,24 @@ CC=g++ CDEFINES= -SOURCES=Dispatcher.cpp Mode.cpp precomp.cpp profanity.cpp SpeedSample.cpp -OBJECTS=$(SOURCES:.cpp=.o) + +SRCDIR=src +KERNELDIR=kernels +OBJDIR=build +BINDIR=bin + +SOURCES=$(addprefix $(SRCDIR)/,Dispatcher.cpp Mode.cpp precomp.cpp profanity.cpp SpeedSample.cpp) +OBJECTS=$(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES)) + +# The kernels are copied next to the binary because that is the first place +# profanity2 looks for them, which makes bin/ self-contained: it can be copied +# elsewhere and run from any working directory. +KERNELS=$(addprefix $(BINDIR)/,keccak.cl profanity.cl) # On Windows the OS environment variable is always set to Windows_NT. # Checking it first avoids calling `uname`, which is unavailable outside # of MSYS2/Cygwin shells (e.g. when using mingw32-make from cmd.exe). ifeq ($(OS),Windows_NT) - EXECUTABLE=profanity2.exe + EXECUTABLE=$(BINDIR)/profanity2.exe # -mcmodel=large is not reliably supported by MinGW GCC on Windows # targets, and htonl/ntohl live in ws2_32 there. # -static bundles the MinGW runtimes (libstdc++, libgcc, winpthread) so @@ -17,14 +28,22 @@ ifeq ($(OS),Windows_NT) LDFLAGS=-s -static -l:libOpenCL.dll.a -lws2_32 CFLAGS=-c -std=c++11 -Wall -mmmx -O2 # MSYSTEM is set inside MSYS2 shells, where unix commands are available. + # Outside of them the recipes have to speak cmd.exe, which wants + # backslashes and refuses to mkdir a directory that already exists. ifdef MSYSTEM - RM=rm -f + MKDIR=mkdir -p $1 + COPY=cp -f $1 $2 + RMDIR=rm -rf $1 else - RM=del /Q + MKDIR=if not exist "$(subst /,\,$1)" mkdir "$(subst /,\,$1)" + COPY=copy /Y "$(subst /,\,$1)" "$(subst /,\,$2)" >NUL + RMDIR=if exist "$(subst /,\,$1)" rmdir /S /Q "$(subst /,\,$1)" endif else - EXECUTABLE=profanity2.x64 - RM=rm -f + EXECUTABLE=$(BINDIR)/profanity2.x64 + MKDIR=mkdir -p $1 + COPY=cp -f $1 $2 + RMDIR=rm -rf $1 UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) LDFLAGS=-framework OpenCL @@ -35,13 +54,22 @@ else endif endif -all: $(SOURCES) $(EXECUTABLE) +.PHONY: all clean -$(EXECUTABLE): $(OBJECTS) +all: $(EXECUTABLE) $(KERNELS) + +$(EXECUTABLE): $(OBJECTS) | $(BINDIR) $(CC) $(OBJECTS) $(LDFLAGS) -o $@ -.cpp.o: +$(OBJDIR)/%.o: $(SRCDIR)/%.cpp | $(OBJDIR) $(CC) $(CFLAGS) $(CDEFINES) $< -o $@ +$(BINDIR)/%.cl: $(KERNELDIR)/%.cl | $(BINDIR) + $(call COPY,$<,$@) + +$(OBJDIR) $(BINDIR): + $(call MKDIR,$@) + clean: - $(RM) *.o + $(call RMDIR,$(OBJDIR)) + $(call RMDIR,$(BINDIR)) diff --git a/README.md b/README.md index 70396d0..a2260de 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Profanity is a high performance (probably the fastest!) vanity address generator for Ethereum. Create cool customized addresses that you never realized you needed! Recieve Ether in style! Wow! -![Screenshot](/img/screenshot.png?raw=true "Wow! That's a lot of zeros!") +![Screenshot](/docs/img/screenshot.png?raw=true "Wow! That's a lot of zeros!") # Important to know @@ -65,7 +65,8 @@ xcode-select --install # skip if already installed make ``` -This produces `profanity2.x64` in the repository root. Works on both Intel and +This produces `bin/profanity2.x64`, with the two OpenCL kernels copied next to +it so the directory can be moved elsewhere as is. Works on both Intel and Apple Silicon (M1/M2/M3/M4) Macs. ### Ubuntu / Linux @@ -97,6 +98,23 @@ docker run --rm --gpus all ghcr.io/1inch/profanity2:latest --matching dead -z $P Since the tool only needs your public key, that machine does not have to be yours. See [docs/VASTAI.md](docs/VASTAI.md) for renting a GPU on vast.ai and running the image there with your own parameters. +### Repository layout + +``` +src/ C++ sources of the host program +kernels/ the OpenCL kernels, keccak.cl and profanity.cl +bin/ what make produces: the binary and a copy of the kernels +build/ object files +docker/ the shipping image and its entrypoint +bench/ tooling that compares the speed of two revisions on one GPU +tests/ correctness tests and a microbenchmark for the mp-math kernels +docs/ build guides for Ubuntu and Windows, and the vast.ai guide +``` + +`make` copies the kernels into `bin/` because that is the first place the binary +looks for them, so it can be started from any directory. It still falls back to +the working directory, which is where releases before this layout kept them. + # Usage ``` usage: ./profanity2 [OPTIONS] @@ -196,10 +214,10 @@ and better results: ```bash # 0x00000... (as many leading zeros as possible) -./profanity2.x64 --leading 0 -z $PUBLIC_KEY +./bin/profanity2.x64 --leading 0 -z $PUBLIC_KEY # 0xaaaaa... (as many leading "a"s as possible) -./profanity2.x64 --leading a -z $PUBLIC_KEY +./bin/profanity2.x64 --leading a -z $PUBLIC_KEY ``` ### Exact pattern (`--matching`) @@ -212,31 +230,31 @@ matches anything. The score is the number of matched fixed positions. ```bash # 0xdead... -./profanity2.x64 --matching dead -z $PUBLIC_KEY +./bin/profanity2.x64 --matching dead -z $PUBLIC_KEY ``` **Suffix** — pad the beginning of a full 40-character pattern with `X` wildcards: ```bash # 0x...999999 -./profanity2.x64 --matching XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX999999 -z $PUBLIC_KEY +./bin/profanity2.x64 --matching XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX999999 -z $PUBLIC_KEY ``` **Prefix and suffix at the same time** — fix both ends, wildcard the middle: ```bash # 0x1111...2222 -./profanity2.x64 --matching 1111XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX2222 -z $PUBLIC_KEY +./bin/profanity2.x64 --matching 1111XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX2222 -z $PUBLIC_KEY # 0xbad...bad -./profanity2.x64 --matching badXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXbad -z $PUBLIC_KEY +./bin/profanity2.x64 --matching badXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXbad -z $PUBLIC_KEY ``` **Arbitrary positions** — any mix of fixed characters and wildcards works: ```bash # 0xXXXXcafeXXXX...XXXX (characters 5-8 are "cafe") -./profanity2.x64 --matching XXXXcafeXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -z $PUBLIC_KEY +./bin/profanity2.x64 --matching XXXXcafeXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -z $PUBLIC_KEY ``` Note: only results improving the best score so far are printed, so after a result matching @@ -253,10 +271,10 @@ each one: ```bash # Every address 0x1337...c0de found, not just the first one -./profanity2.x64 --exact 1337XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXc0de -z $PUBLIC_KEY +./bin/profanity2.x64 --exact 1337XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXc0de -z $PUBLIC_KEY # Every address with at least five leading zeros -./profanity2.x64 --exact 00000XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -z $PUBLIC_KEY +./bin/profanity2.x64 --exact 00000XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -z $PUBLIC_KEY ``` The first matches take longer to appear than with `--matching` (partial matches are not @@ -270,13 +288,13 @@ Score on the total amount of matching characters anywhere in the address: ```bash # As many "0" characters as possible, e.g. 0x00aa0e050bb03c0b066e00c0f70a03d0d000b0d7 -./profanity2.x64 --zeros -z $PUBLIC_KEY +./bin/profanity2.x64 --zeros -z $PUBLIC_KEY # Only letters (a-f), e.g. 0xffcadbfaecfcdeaddeedabadfeedfacebeefcafe -./profanity2.x64 --letters -z $PUBLIC_KEY +./bin/profanity2.x64 --letters -z $PUBLIC_KEY # Only numbers (0-9), e.g. 0x8896339129744478701529940603494328137361 -./profanity2.x64 --numbers -z $PUBLIC_KEY +./bin/profanity2.x64 --numbers -z $PUBLIC_KEY ``` ### Ranges (`--leading-range`, `--range`) @@ -286,27 +304,27 @@ Score on characters within a given hex range, set with `-m/--min` and `-M/--max` ```bash # Leading characters in range 0-1, e.g. 0x0110100... -./profanity2.x64 --leading-range -m 0 -M 1 -z $PUBLIC_KEY +./bin/profanity2.x64 --leading-range -m 0 -M 1 -z $PUBLIC_KEY # Leading characters in range a-c, e.g. 0xcbabacc... -./profanity2.x64 --leading-range -m 10 -M 12 -z $PUBLIC_KEY +./bin/profanity2.x64 --leading-range -m 10 -M 12 -z $PUBLIC_KEY # Characters in range 0-1 anywhere in the address -./profanity2.x64 --range -m 0 -M 1 -z $PUBLIC_KEY +./bin/profanity2.x64 --range -m 0 -M 1 -z $PUBLIC_KEY ``` ### Other scoring modes (`--mirror`, `--leading-doubles`, `--zero-bytes`) ```bash # Address mirrored around its center, e.g. 0x...abccba... -./profanity2.x64 --mirror -z $PUBLIC_KEY +./bin/profanity2.x64 --mirror -z $PUBLIC_KEY # Leading pairs of identical characters, e.g. 0x00fFcc55... -./profanity2.x64 --leading-doubles -z $PUBLIC_KEY +./bin/profanity2.x64 --leading-doubles -z $PUBLIC_KEY # As many zero BYTES (pairs "00" at even positions) as possible; such addresses # save gas when used in calldata, e.g. 0x00815e00c0fd4a2d00ae00fa00e300ee00fc0034 -./profanity2.x64 --zero-bytes -z $PUBLIC_KEY +./bin/profanity2.x64 --zero-bytes -z $PUBLIC_KEY ``` ### Vanity contract address (`--contract`) @@ -316,20 +334,20 @@ zeroth transaction** of the found account instead of the account address itself: ```bash # Account whose first deployed contract gets a 0x00000... address -./profanity2.x64 --contract --leading 0 -z $PUBLIC_KEY +./bin/profanity2.x64 --contract --leading 0 -z $PUBLIC_KEY # Account whose first deployed contract address has the most zero bytes -./profanity2.x64 --contract --zero-bytes -z $PUBLIC_KEY +./bin/profanity2.x64 --contract --zero-bytes -z $PUBLIC_KEY ``` ### Benchmark and device control ```bash # Measure hashrate without any scoring -./profanity2.x64 --benchmark -z $PUBLIC_KEY +./bin/profanity2.x64 --benchmark -z $PUBLIC_KEY # Multiple GPUs are used automatically; skip a device (e.g. an integrated GPU) by index -./profanity2.x64 --leading 0 -s 1 -z $PUBLIC_KEY +./bin/profanity2.x64 --leading 0 -s 1 -z $PUBLIC_KEY ``` ### Benchmarks - Current version diff --git a/bench/.dockerignore b/bench/.dockerignore new file mode 100644 index 0000000..fada043 --- /dev/null +++ b/bench/.dockerignore @@ -0,0 +1,8 @@ +# The benchmark image is built with this directory as its context, and the only +# file it copies in is run-benchmark.sh. Everything else just invalidates the +# layer cache when it changes. +README.md +logs +build.sh +publish.sh +prepare-native.sh diff --git a/bench/Dockerfile b/bench/Dockerfile index 8b9692d..4dedaab 100644 --- a/bench/Dockerfile +++ b/bench/Dockerfile @@ -2,8 +2,7 @@ # # Builds a self-contained image holding two revisions of profanity2 so their # speed can be compared back to back on the same GPU. This image is a -# measuring tool, not the shipping image - see the Dockerfile in the -# repository root for that one. +# measuring tool, not the shipping image - see docker/Dockerfile for that one. # # docker build --build-arg REF_A=master --build-arg REF_B=pr/57 -t bench bench/ # @@ -31,7 +30,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # explicitly, otherwise a commit that only exists in a pull request cannot be # resolved. The last step records whether the two revisions time a round the # same way: if they do not, the speeds they print come from different clocks -# and the runner has to warn about it. +# and the runner has to warn about it. SpeedSample.cpp is looked for in two +# places because revisions from before the source tree was split into src/ and +# kernels/ keep it in the root of the tree. RUN set -eu; \ echo "cachebust: ${CACHEBUST}"; \ test -n "${REF_A}" || { echo "error: REF_A build argument is empty" >&2; exit 1; }; \ @@ -55,7 +56,9 @@ RUN set -eu; \ git -C /repo rev-parse --short=7 "${sha}" > "/src/${slot}.sha"; \ echo "${slot}: ${ref} -> ${sha}"; \ done; \ - if cmp -s /src/a/SpeedSample.cpp /src/b/SpeedSample.cpp; then \ + timer_a="$(ls /src/a/src/SpeedSample.cpp /src/a/SpeedSample.cpp 2>/dev/null | head -1)"; \ + timer_b="$(ls /src/b/src/SpeedSample.cpp /src/b/SpeedSample.cpp 2>/dev/null | head -1)"; \ + if [ -n "${timer_a}" ] && [ -n "${timer_b}" ] && cmp -s "${timer_a}" "${timer_b}"; then \ echo same > /src/timer.state; \ else \ echo differs > /src/timer.state; \ @@ -75,6 +78,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY --from=src /src /src RUN make -C /src/a -j"$(nproc)" && make -C /src/b -j"$(nproc)" +# A revision from before the source tree was reorganized leaves the binary and +# the kernels in the root of its tree, a newer one leaves them in bin/. COPY +# cannot choose between the two, so both are collected here and the runtime +# stage sees a single layout. +RUN set -eu; \ + for slot in a b; do \ + mkdir -p "/out/${slot}"; \ + if [ -d "/src/${slot}/bin" ]; then \ + cp "/src/${slot}/bin/profanity2.x64" "/src/${slot}/bin/keccak.cl" "/src/${slot}/bin/profanity.cl" "/out/${slot}/"; \ + else \ + cp "/src/${slot}/profanity2.x64" "/src/${slot}/keccak.cl" "/src/${slot}/profanity.cl" "/out/${slot}/"; \ + fi; \ + done + # --------------------------------------------------------------------------- # Runtime # --------------------------------------------------------------------------- @@ -100,10 +117,10 @@ ENV NVIDIA_VISIBLE_DEVICES=all \ NVIDIA_DRIVER_CAPABILITIES=compute,utility # Each revision keeps its own directory: profanity2 loads keccak.cl and -# profanity.cl from the working directory and caches the compiled kernel -# there, so the two builds must never share one. -COPY --from=build /src/a/profanity2.x64 /src/a/keccak.cl /src/a/profanity.cl /opt/bench/a/ -COPY --from=build /src/b/profanity2.x64 /src/b/keccak.cl /src/b/profanity.cl /opt/bench/b/ +# profanity.cl from beside the binary and caches the compiled kernel there, so +# the two builds must never share one. +COPY --from=build /out/a /opt/bench/a +COPY --from=build /out/b /opt/bench/b COPY --from=src /src/a.ref /src/a.sha /src/b.ref /src/b.sha /src/timer.state /opt/bench/ COPY run-benchmark.sh /usr/local/bin/bench diff --git a/bench/README.md b/bench/README.md index 933259c..1dcd894 100644 --- a/bench/README.md +++ b/bench/README.md @@ -1,6 +1,6 @@ # Comparing the speed of two profanity2 revisions -This directory builds a **separate image** from the one in the repository root. The image in the root ships profanity2; this one measures it. It contains two revisions of profanity2 side by side and runs them alternately on the same GPU, so a change can be judged without trusting that two rented machines are equally fast. +This directory builds a **separate image** from the one in [docker/](../docker/Dockerfile). That one ships profanity2; this one measures it. It contains two revisions of profanity2 side by side and runs them alternately on the same GPU, so a change can be judged without trusting that two rented machines are equally fast. There are two ways to put the two revisions side by side. [bench/build.sh](build.sh) bakes them into an image for a rented NVIDIA GPU, which is the rest of this document; [bench/prepare-native.sh](prepare-native.sh) builds them on the machine you are sitting at, which is the only way to measure an Apple GPU - see [Running natively](#running-natively-no-docker). Both then hand over to the same [run-benchmark.sh](run-benchmark.sh). @@ -199,3 +199,7 @@ docker run --rm --gpus all IMAGE --mode exact ``` `--mode exact` uses `--exact `, which prints every address matching the mask, and derives the throughput from how many appear per second: `rate = matches / seconds * 16^fixed`. Nothing in that number comes from the program's own timer. It is a counting measurement, so its precision is `1/sqrt(matches)`: the default 7-character mask on a 1 GH/s card yields about four matches per second, so a 120 second window gives roughly 450 matches and 5% precision. Widen `--seconds` to resolve smaller differences. + +## Recorded runs + +[logs/](logs) holds the output of comparisons that were actually run, one file per machine, named after the GPU and where it was rented. They are the raw output of the runner, so each one still carries the revisions it compared and the settings it used. diff --git a/bench-logs/1xRTX3060.vastai.log b/bench/logs/1xRTX3060.vastai.log similarity index 100% rename from bench-logs/1xRTX3060.vastai.log rename to bench/logs/1xRTX3060.vastai.log diff --git a/bench-logs/1xRTX5090.vastai.log b/bench/logs/1xRTX5090.vastai.log similarity index 100% rename from bench-logs/1xRTX5090.vastai.log rename to bench/logs/1xRTX5090.vastai.log diff --git a/bench-logs/2xRTX3060.vastai.log b/bench/logs/2xRTX3060.vastai.log similarity index 100% rename from bench-logs/2xRTX3060.vastai.log rename to bench/logs/2xRTX3060.vastai.log diff --git a/bench-logs/8xRTX5090.vastai.log b/bench/logs/8xRTX5090.vastai.log similarity index 100% rename from bench-logs/8xRTX5090.vastai.log rename to bench/logs/8xRTX5090.vastai.log diff --git a/bench-logs/M4Max.mac.log b/bench/logs/M4Max.mac.log similarity index 100% rename from bench-logs/M4Max.mac.log rename to bench/logs/M4Max.mac.log diff --git a/bench/prepare-native.sh b/bench/prepare-native.sh index 11888fb..88309e9 100755 --- a/bench/prepare-native.sh +++ b/bench/prepare-native.sh @@ -192,7 +192,14 @@ build_slot() { make -C "$src" -j"$jobs_count" >&2 || die "$slot: build of $short failed" mkdir -p "$dst" - cp "$src/profanity2.x64" "$src/keccak.cl" "$src/profanity.cl" "$dst/" + # A revision from before the source tree was reorganized leaves the + # binary and the kernels in the root of its tree, a newer one leaves + # them in bin/. + if [ -d "$src/bin" ]; then + cp "$src/bin/profanity2.x64" "$src/bin/keccak.cl" "$src/bin/profanity.cl" "$dst/" + else + cp "$src/profanity2.x64" "$src/keccak.cl" "$src/profanity.cl" "$dst/" + fi fi printf '%s' "$ref" > "$WORKDIR/$slot.ref" @@ -206,7 +213,28 @@ build_slot b "$REF_B" "$SHA_B" "$SHORT_B" # Whether the two revisions time a round the same way. If they do not, the # speeds they print come from different clocks and the runner warns about it. -if git -C "$SRC_REPO" diff --quiet "$SHA_A" "$SHA_B" -- SpeedSample.cpp; then +# The two are compared by content, through the blob each revision records for +# the file, rather than by path: a revision from before the source tree was +# reorganized keeps SpeedSample.cpp in the root of the tree, and a file that +# only moved still times a round exactly the same way. +timer_blob() { + local sha="$1" path blob + + for path in src/SpeedSample.cpp SpeedSample.cpp; do + blob="$(git -C "$SRC_REPO" rev-parse --verify --quiet "$sha:$path" 2>/dev/null || true)" + if [ -n "$blob" ]; then + printf '%s\n' "$blob" + return 0 + fi + done + + return 1 +} + +TIMER_A="$(timer_blob "$SHA_A" || true)" +TIMER_B="$(timer_blob "$SHA_B" || true)" + +if [ -n "$TIMER_A" ] && [ "$TIMER_A" = "$TIMER_B" ]; then printf 'same\n' > "$WORKDIR/timer.state" else printf 'differs\n' > "$WORKDIR/timer.state" diff --git a/Dockerfile b/docker/Dockerfile similarity index 87% rename from Dockerfile rename to docker/Dockerfile index f603894..a747b4e 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -13,7 +13,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /src COPY Makefile ./ -COPY *.cpp *.hpp *.cl ./ +COPY src ./src +COPY kernels ./kernels RUN make -j"$(nproc)" # --------------------------------------------------------------------------- @@ -44,10 +45,11 @@ RUN mkdir -p /etc/OpenCL/vendors \ ENV NVIDIA_VISIBLE_DEVICES=all \ NVIDIA_DRIVER_CAPABILITIES=compute,utility -# profanity2 loads keccak.cl/profanity.cl and stores its compiled kernel cache -# relative to the working directory, so the binary has to run from here. +# The build leaves the binary and the two kernels together in bin/, which is +# the layout profanity2 looks for first, so it finds them wherever it is +# started from. Its compiled kernel cache is written here as well. WORKDIR /opt/profanity2 -COPY --from=build /src/profanity2.x64 /src/keccak.cl /src/profanity.cl ./ +COPY --from=build /src/bin/ ./ COPY LICENSE ./ COPY docker/entrypoint.sh /usr/local/bin/profanity2-entrypoint RUN chmod +x /usr/local/bin/profanity2-entrypoint && mkdir -p /workspace diff --git a/docs/BUILD_UBUNTU.md b/docs/BUILD_UBUNTU.md index 9bc34b9..3198c92 100644 --- a/docs/BUILD_UBUNTU.md +++ b/docs/BUILD_UBUNTU.md @@ -76,12 +76,13 @@ driver installation first. make ``` -This produces the `profanity2.x64` executable in the repository root. +This produces `bin/profanity2.x64`, with the two OpenCL kernels copied next to +it. Object files land in `build/`. ## 5. Run ```bash -./profanity2.x64 --leading 0 -z HEX_PUBLIC_KEY_128_CHARS_LONG +./bin/profanity2.x64 --leading 0 -z HEX_PUBLIC_KEY_128_CHARS_LONG ``` See the [README](../README.md) for how to generate the public key for the `-z` diff --git a/docs/BUILD_WINDOWS.md b/docs/BUILD_WINDOWS.md index d9228c7..c49727a 100644 --- a/docs/BUILD_WINDOWS.md +++ b/docs/BUILD_WINDOWS.md @@ -36,7 +36,8 @@ make ``` The `Makefile` detects Windows automatically (via the `OS=Windows_NT` environment -variable) and produces `profanity2.exe`. The MinGW runtimes (`libstdc++`, `libgcc`, +variable) and produces `bin\profanity2.exe`, with the two OpenCL kernels copied +next to it. The MinGW runtimes (`libstdc++`, `libgcc`, `winpthread`) are linked statically, so the resulting exe is self-contained and runs outside the MSYS2 shell — only `OpenCL.dll` is loaded dynamically, and that one ships with your GPU driver. @@ -44,8 +45,10 @@ one ships with your GPU driver. If you prefer to build without `make`, the equivalent direct command is: ```bash -g++ -std=c++11 -Wall -O2 Dispatcher.cpp Mode.cpp precomp.cpp profanity.cpp SpeedSample.cpp \ - -static -l:libOpenCL.dll.a -lws2_32 -o profanity2.exe +mkdir -p bin +g++ -std=c++11 -Wall -O2 src/Dispatcher.cpp src/Mode.cpp src/precomp.cpp src/profanity.cpp src/SpeedSample.cpp \ + -static -l:libOpenCL.dll.a -lws2_32 -o bin/profanity2.exe +cp kernels/keccak.cl kernels/profanity.cl bin/ ``` (`-l:libOpenCL.dll.a` names the OpenCL import library explicitly because with @@ -57,7 +60,7 @@ Your GPU driver must be installed (NVIDIA and AMD drivers include the OpenCL runtime on Windows). Then: ```bash -./profanity2.exe --leading 0 -z HEX_PUBLIC_KEY_128_CHARS_LONG +./bin/profanity2.exe --leading 0 -z HEX_PUBLIC_KEY_128_CHARS_LONG ``` The executable is statically linked against the MinGW runtimes, so it can be @@ -96,8 +99,8 @@ works, e.g. the [CUDA Toolkit](https://developer.nvidia.com/cuda-downloads) g++ -std=c++11 -Wall -O2 \ -I"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.4/include" \ -L"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.4/lib/x64" \ - Dispatcher.cpp Mode.cpp precomp.cpp profanity.cpp SpeedSample.cpp \ - -lOpenCL -lws2_32 -o profanity2.exe + src/Dispatcher.cpp src/Mode.cpp src/precomp.cpp src/profanity.cpp src/SpeedSample.cpp \ + -lOpenCL -lws2_32 -o bin/profanity2.exe ``` The same works through `make`: diff --git a/docs/VASTAI.md b/docs/VASTAI.md index ecccef8..b5f9ddc 100644 --- a/docs/VASTAI.md +++ b/docs/VASTAI.md @@ -1,6 +1,6 @@ # Running profanity2 on a rented GPU (vast.ai) -The [Dockerfile](../Dockerfile) in the repository root builds an image that contains the compiled binary, the OpenCL kernels and the OpenCL runtime glue needed inside a container. Nothing has to be installed on the rented machine: you pick the image, type the profanity2 options into the template and start the instance. +The [Dockerfile](../docker/Dockerfile) builds an image that contains the compiled binary, the OpenCL kernels and the OpenCL runtime glue needed inside a container. Nothing has to be installed on the rented machine: you pick the image, type the profanity2 options into the template and start the instance. The image is published as `ghcr.io/1inch/profanity2` (see [Building your own image](#building-your-own-image) if you prefer your own registry). It works on any Docker host with an NVIDIA GPU, vast.ai is just the cheapest way to rent one. @@ -136,7 +136,7 @@ Build your own if you have local changes, if you want a revision that is not pub ```bash git clone https://github.com/1inch/profanity2 cd profanity2 -docker build --platform linux/amd64 -t profanity2 . +docker build --platform linux/amd64 -f docker/Dockerfile -t profanity2 . ``` `--platform linux/amd64` is not optional. The Linux branch of the Makefile passes `-mmmx` and `-mcmodel=large`, which do not exist on arm64, so a native build on an Apple Silicon Mac fails with `unrecognized command-line option '-mmmx'`. Rented GPU machines are x86_64 anyway. Under emulation the build takes a few minutes rather than the half minute it needs on an x86 host. @@ -172,7 +172,7 @@ In the GUI the same credentials go into the *Docker login* field of the template ```bash IMAGE=ttl.sh/profanity2-$(uuidgen | tr '[:upper:]' '[:lower:]'):24h -docker build --platform linux/amd64 -t "$IMAGE" . +docker build --platform linux/amd64 -f docker/Dockerfile -t "$IMAGE" . docker push "$IMAGE" echo "$IMAGE" ``` diff --git a/img/screenshot.png b/docs/img/screenshot.png similarity index 100% rename from img/screenshot.png rename to docs/img/screenshot.png diff --git a/keccak.cl b/kernels/keccak.cl similarity index 100% rename from keccak.cl rename to kernels/keccak.cl diff --git a/profanity.cl b/kernels/profanity.cl similarity index 100% rename from profanity.cl rename to kernels/profanity.cl diff --git a/ArgParser.hpp b/src/ArgParser.hpp similarity index 100% rename from ArgParser.hpp rename to src/ArgParser.hpp diff --git a/CLMemory.hpp b/src/CLMemory.hpp similarity index 100% rename from CLMemory.hpp rename to src/CLMemory.hpp diff --git a/Dispatcher.cpp b/src/Dispatcher.cpp similarity index 100% rename from Dispatcher.cpp rename to src/Dispatcher.cpp diff --git a/Dispatcher.hpp b/src/Dispatcher.hpp similarity index 100% rename from Dispatcher.hpp rename to src/Dispatcher.hpp diff --git a/Mode.cpp b/src/Mode.cpp similarity index 100% rename from Mode.cpp rename to src/Mode.cpp diff --git a/Mode.hpp b/src/Mode.hpp similarity index 100% rename from Mode.hpp rename to src/Mode.hpp diff --git a/SpeedSample.cpp b/src/SpeedSample.cpp similarity index 100% rename from SpeedSample.cpp rename to src/SpeedSample.cpp diff --git a/SpeedSample.hpp b/src/SpeedSample.hpp similarity index 100% rename from SpeedSample.hpp rename to src/SpeedSample.hpp diff --git a/help.hpp b/src/help.hpp similarity index 100% rename from help.hpp rename to src/help.hpp diff --git a/lexical_cast.hpp b/src/lexical_cast.hpp similarity index 100% rename from lexical_cast.hpp rename to src/lexical_cast.hpp diff --git a/precomp.cpp b/src/precomp.cpp similarity index 100% rename from precomp.cpp rename to src/precomp.cpp diff --git a/precomp.hpp b/src/precomp.hpp similarity index 100% rename from precomp.hpp rename to src/precomp.hpp diff --git a/profanity.cpp b/src/profanity.cpp similarity index 82% rename from profanity.cpp rename to src/profanity.cpp index 301d0fb..5cae385 100644 --- a/profanity.cpp +++ b/src/profanity.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,17 @@ #include // Included to get topology to get an actual unique identifier per device #endif +// Asking the OS where the running executable is, see getExecutablePath below. +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#elif defined(__APPLE__) || defined(__MACOSX) +#include +#else +#include +#endif + #define CL_DEVICE_PCI_BUS_ID_NV 0x4008 #define CL_DEVICE_PCI_SLOT_ID_NV 0x4009 @@ -25,12 +37,69 @@ #include "Mode.hpp" #include "help.hpp" -std::string readFile(const char * const szFilename) -{ - std::ifstream in(szFilename, std::ios::in | std::ios::binary); - std::ostringstream contents; - contents << in.rdbuf(); - return contents.str(); +// Directory the running executable lives in, with a trailing separator, or +// empty when it could not be determined. Set once at the start of main. +std::string g_strExecutableDir; + +std::string directoryOf(const std::string & strPath) { + const std::string::size_type pos = strPath.find_last_of("/\\"); + return pos == std::string::npos ? std::string() : strPath.substr(0, pos + 1); +} + +std::string getExecutablePath() { +#if defined(_WIN32) + char szPath[MAX_PATH]; + const DWORD length = GetModuleFileNameA(NULL, szPath, MAX_PATH); + if (length > 0 && length < MAX_PATH) { + return std::string(szPath, length); + } +#elif defined(__APPLE__) || defined(__MACOSX) + uint32_t length = 0; + _NSGetExecutablePath(NULL, &length); // Fails on purpose, only to report the size + if (length > 0) { + std::vector vPath(length, '\0'); + if (_NSGetExecutablePath(vPath.data(), &length) == 0) { + return std::string(vPath.data()); + } + } +#else + std::vector vPath(4096, '\0'); + const ssize_t length = readlink("/proc/self/exe", vPath.data(), vPath.size() - 1); + if (length > 0) { + return std::string(vPath.data(), length); + } +#endif + return std::string(); +} + +// Kernel sources sit next to the executable in the container image and in the +// directories the benchmark harness builds, and in kernels/ in a source tree. +// The working directory is tried last: that is where every release before the +// source tree was split into src/ and kernels/ kept them. +std::string readKernel(const std::string & strName) { + std::vector vCandidates; + if (!g_strExecutableDir.empty()) { + vCandidates.push_back(g_strExecutableDir + strName); + vCandidates.push_back(g_strExecutableDir + "../kernels/" + strName); + } + vCandidates.push_back(strName); + vCandidates.push_back("kernels/" + strName); + + for (const auto & strCandidate : vCandidates) { + std::ifstream in(strCandidate.c_str(), std::ios::in | std::ios::binary); + if (in) { + std::ostringstream contents; + contents << in.rdbuf(); + return contents.str(); + } + } + + std::string strTried; + for (const auto & strCandidate : vCandidates) { + strTried += "\n " + strCandidate; + } + + throw std::runtime_error("could not open " + strName + ", looked in:" + strTried); } std::vector getAllDevices(cl_device_type deviceType = CL_DEVICE_TYPE_GPU) { @@ -162,7 +231,7 @@ bool printResult(const cl_int err) { std::string getDeviceCacheFilename(cl_device_id & d, const size_t & inverseSize) { const auto uniqueId = getUniqueDeviceIdentifier(d); - return "cache-opencl." + toString(inverseSize) + "." + toString(uniqueId); + return g_strExecutableDir + "cache-opencl." + toString(inverseSize) + "." + toString(uniqueId); } int main(int argc, char * * argv) { @@ -172,6 +241,11 @@ int main(int argc, char * * argv) { // now it only advances provided public key to a random offset to find vanity address try { + g_strExecutableDir = directoryOf(getExecutablePath()); + if (g_strExecutableDir.empty() && argc > 0) { + g_strExecutableDir = directoryOf(argv[0]); + } + ArgParser argp(argc, argv); bool bHelp = false; bool bModeBenchmark = false; @@ -350,8 +424,8 @@ int main(int argc, char * * argv) { } else { // Create a program from the kernel source std::cout << " Compiling kernel..." << std::flush; - const std::string strKeccak = readFile("keccak.cl"); - const std::string strVanity = readFile("profanity.cl"); + const std::string strKeccak = readKernel("keccak.cl"); + const std::string strVanity = readKernel("profanity.cl"); const char * szKernels[] = { strKeccak.c_str(), strVanity.c_str() }; clProgram = clCreateProgramWithSource(clContext, sizeof(szKernels) / sizeof(char *), szKernels, NULL, &errorCode); diff --git a/types.hpp b/src/types.hpp similarity index 100% rename from types.hpp rename to src/types.hpp diff --git a/tests/Makefile b/tests/Makefile index a9b29c7..fed1cbd 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -22,10 +22,10 @@ endif all: $(EXECUTABLES) -test_correctness_pr49.x64: test_correctness_pr49.cpp testutil.hpp ../types.hpp +test_correctness_pr49.x64: test_correctness_pr49.cpp testutil.hpp ../src/types.hpp $(CC) $(CFLAGS) $(CDEFINES) test_correctness_pr49.cpp $(LDFLAGS) -o $@ -bench_mod_mul_pr49.x64: bench_mod_mul_pr49.cpp testutil.hpp ../types.hpp +bench_mod_mul_pr49.x64: bench_mod_mul_pr49.cpp testutil.hpp ../src/types.hpp $(CC) $(CFLAGS) $(CDEFINES) bench_mod_mul_pr49.cpp $(LDFLAGS) -o $@ clean: diff --git a/tests/testutil.hpp b/tests/testutil.hpp index 362ecc9..d6cf0e1 100644 --- a/tests/testutil.hpp +++ b/tests/testutil.hpp @@ -5,7 +5,8 @@ * ============ * Shared helpers for the mp-math correctness tests and benchmarks: * - OpenCL boilerplate (context/device selection, program build from - * keccak.cl + profanity.cl + tests/harness.cl, mirroring profanity.cpp) + * kernels/keccak.cl + kernels/profanity.cl + tests/harness.cl, mirroring + * profanity.cpp) * - Host-side 256/512-bit reference arithmetic used to verify the kernels * against an independent implementation. */ @@ -26,7 +27,7 @@ #include #include -#include "../types.hpp" +#include "../src/types.hpp" /* ------------------------------------------------------------------------ */ /* OpenCL helpers */ @@ -108,8 +109,8 @@ inline ClSetup clSetup() { clCheck(errorCode, "clCreateCommandQueue"); // Same source assembly as profanity.cpp, plus the test harness - const std::string strKeccak = readFile("keccak.cl"); - const std::string strVanity = readFile("profanity.cl"); + const std::string strKeccak = readFile("kernels/keccak.cl"); + const std::string strVanity = readFile("kernels/profanity.cl"); const std::string strHarness = readFile("tests/harness.cl"); const char * szKernels[] = { strKeccak.c_str(), strVanity.c_str(), strHarness.c_str() };