├── README.md ├── appveyor.yml ├── client.sh ├── go.mod ├── go.sum ├── lc0_main.go └── src └── client └── client_http.go /README.md: -------------------------------------------------------------------------------- 1 | # Compiling 2 | 3 | You will need to install Go 1.9 or later. 4 | 5 | Then, make sure to set up your GOPATH properly, eg. here is mine: 6 | ``` 7 | export GOPATH=${HOME}/go:${HOME}/src/lczero-client 8 | ``` 9 | Here, I've set my system install of go as the first entry, and then the lczero-client directory as the second. 10 | 11 | Pre-reqs: 12 | ``` 13 | # (Bug workaround, using Tilps instead) 14 | # go get -u github.com/notnil/chess 15 | go get -u github.com/Tilps/chess 16 | go get -u github.com/nightlyone/lockfile 17 | 18 | ``` 19 | 20 | Pull or download the `master` branch 21 | 22 | Then to produce a `lczero-client` executable: 23 | `go build lc0_main.go` for the `lc0` client 24 | 25 | If you get 26 | `.\lc0_main.go:1048:5: undefined: chess.GetLibraryVersion` 27 | you have a cached old version of Tilps/chess and need to run the Pre-reqs again. 28 | 29 | # Running 30 | 31 | First copy the `lc0` executable into the same folder as the `lczero-client` executable. 32 | 33 | Then, run! Username and password are required parameters. 34 | ``` 35 | ./lczero-client --user=myusername --password=mypassword 36 | ``` 37 | 38 | For testing, you can also point the client at a different server: 39 | ``` 40 | ./lczero-client --hostname=http://127.0.0.1:8080 --user=test --password=asdf 41 | ``` 42 | 43 | # Cross-compiling 44 | 45 | One of the main reasons I picked go was it's amazing support for cross-compiling. 46 | 47 | Pre-reqs: 48 | ``` 49 | GOOS=windows GOARCH=amd64 go install 50 | GOOS=darwin GOARCH=amd64 go install 51 | GOOS=linux GOARCH=amd64 go install 52 | ``` 53 | 54 | Building the client for each platform: 55 | ``` 56 | GOOS=windows GOARCH=amd64 go build -o lczero-client.exe 57 | GOOS=darwin GOARCH=amd64 go build -o lczero-client_mac 58 | GOOS=linux GOARCH=amd64 go build -o lczero-client_linux 59 | ``` 60 | 61 | 62 | # Go module support 63 | 64 | Dependend go modules were added by executing: 65 | 66 | ``` 67 | go get 'github.com/Tilps/chess@master' 68 | ``` 69 | 70 | gives something like: 71 | ``` 72 | go: downloading github.com/Tilps/chess v0.0.0-20200409092358-c35715299813 73 | go: github.com/Tilps/chess master => v0.0.0-20200409092358-c35715299813 74 | ``` 75 | 76 | This version number can then be used in the `go.mod` file 77 | 78 | Whenever you want to update the version do the above `go get` step and there will be a new version number generated that you can put in the existing `go.mod` file. 79 | 80 | Just use the command `go mod download` to update go's module cache. 81 | building should work with `go build lc0_main.go` 82 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | platform: x64 3 | clone_folder: c:\gopath\src\github.com\LeelaChessZero\lczero-client 4 | 5 | environment: 6 | GOPATH: c:\gopath;c:\gopath\src\github.com\LeelaChessZero\lczero-client\ 7 | matrix: 8 | - NAME: .exe 9 | GOOS: windows 10 | - NAME: -linux 11 | GOOS: linux 12 | - NAME: -mac 13 | GOOS: darwin 14 | install: 15 | - go get -u github.com/Tilps/chess 16 | - go get -u github.com/gofrs/flock 17 | build_script: 18 | - go build -o lc0-training-client%NAME% lc0_main.go 19 | artifacts: 20 | - path: lc0-training-client$(NAME) 21 | name: lc0-training-client 22 | deploy: 23 | - provider: GitHub 24 | artifact: lc0-training-client%NAME% 25 | auth_token: 26 | secure: USFAdwQKTXqOXQjCYQfzWvzRpUhvqJLBkN4hbOg+j876vDxGZHt9bMYayb5evePp 27 | on: 28 | appveyor_repo_tag: true 29 | 30 | -------------------------------------------------------------------------------- /client.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Autoupdate script for lc0 client. Use: "./client.sh" followed by any other 3 | # client options. If you need to override system compiler you can run it 4 | # setting CC and CXX, e.g. "CC=gcc-8 CXX=g++-8 ./client.sh". 5 | 6 | trap "exit" INT 7 | 8 | rm -f lc0 9 | git clone --depth 1 --recurse-submodules https://github.com/LeelaChessZero/lc0.git 10 | TAG= 11 | ERR= 12 | FIRST=true 13 | while [ -d lc0 ] 14 | do 15 | cd lc0 16 | 17 | git fetch --tags --depth 1 18 | NEW_TAG=$(git tag --list |grep -v rc |tail -1) 19 | if [ "$TAG" == "$NEW_TAG" ] 20 | then 21 | if [ $ERR -eq 5 ] 22 | then 23 | NEW_TAG=$(git tag --list |grep rc |tail -1) 24 | else 25 | NEW_TAG=$(git tag --list |grep -v rc |tail -2 |head -1) 26 | fi 27 | fi 28 | TAG=$NEW_TAG 29 | git checkout $TAG 30 | 31 | git submodule update --remote 32 | git submodule update --checkout 33 | rm -rf build 34 | meson build --buildtype release -Db_lto=true -Dgtest=false 35 | 36 | cd build 37 | ninja 38 | cd ../.. 39 | 40 | rm -f lc0-training-client-linux 41 | curl -s -L https://github.com/LeelaChessZero/lczero-client/releases/latest | egrep -o '/LeelaChessZero/lczero-client/releases/download/.*/lc0-training-client-linux' | head -n 1 | wget --base=https://github.com/ -i - 42 | chmod +x lc0-training-client-linux 43 | 44 | PATH=lc0/build ./lc0-training-client-linux "$@" 45 | ERR=$? 46 | if [ $ERR -ne 5 ] && $FIRST 47 | then 48 | break 49 | fi 50 | FIRST=false 51 | echo Update needed, starting process shortly. 52 | sleep 60 53 | done 54 | 55 | 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/LeelaChessZero/lczero-client 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Tilps/chess v0.0.0-20200409092358-c35715299813 7 | github.com/nightlyone/lockfile v1.0.1-0.20201014160207-368fd4d5d6ae 8 | ) 9 | 10 | replace github.com/LeelaChessZero/lczero-client => ./ 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Tilps/chess v0.0.0-20200409092358-c35715299813 h1:9SCZ8ooOu4zJEgPuv90hPtkTT5XEIq1K0j2/Jrunsb4= 2 | github.com/Tilps/chess v0.0.0-20200409092358-c35715299813/go.mod h1:p5ClvMMzXkXDcSis21aF6thPWChCbR1YRrTGbEhwA6w= 3 | github.com/nightlyone/lockfile v1.0.1-0.20201014160207-368fd4d5d6ae h1:ZD/X4pxMoJCbiaaxKpSl5hxmr0l0TTiVrF1BPvKThU4= 4 | github.com/nightlyone/lockfile v1.0.1-0.20201014160207-368fd4d5d6ae/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= 5 | -------------------------------------------------------------------------------- /lc0_main.go: -------------------------------------------------------------------------------- 1 | // A new client to work with the lc0 binary. 2 | // 3 | // 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "compress/gzip" 10 | "crypto/rand" 11 | "crypto/sha256" 12 | "encoding/json" 13 | "errors" 14 | "flag" 15 | "fmt" 16 | "io" 17 | "io/ioutil" 18 | "log" 19 | "net/http" 20 | "net/url" 21 | "os" 22 | "os/exec" 23 | "path" 24 | "path/filepath" 25 | "regexp" 26 | "runtime" 27 | "strconv" 28 | "strings" 29 | "sync" 30 | "time" 31 | 32 | "github.com/LeelaChessZero/lczero-client/src/client" 33 | 34 | "github.com/Tilps/chess" 35 | "github.com/gofrs/flock" 36 | ) 37 | 38 | var ( 39 | startTime time.Time 40 | totalGames int 41 | pendingNextGame *client.NextGameResponse 42 | randId int 43 | hasCudnn bool 44 | hasCuda bool 45 | hasOpenCL bool 46 | hasEigen bool 47 | hasDx bool 48 | parallelism32 bool 49 | testedDxNet string 50 | 51 | lc0Exe = "lc0" 52 | defaultLocalHost = "Unknown" 53 | gpuType = "Unknown" 54 | 55 | localHost = flag.String("localhost", "", "Localhost name to send to the server when reporting\n(defaults to Unknown, overridden by the configuration file)") 56 | hostname = flag.String("hostname", "http://api.lczero.org", "Address of the server") 57 | networkMirror = flag.String("network-mirror", "", "Alternative url prefix to download networks from.") 58 | user = flag.String("user", "", "Username") 59 | password = flag.String("password", "", "Password") 60 | gpu = flag.Int("gpu", -1, "GPU to use (ignored if --backend-opts used)") 61 | // debug = flag.Bool("debug", false, "Enable debug mode to see verbose output and save logs") 62 | lc0Args = flag.String("lc0args", "", "") 63 | backopts = flag.String("backend-opts", "", 64 | `Options for the lc0 mux. backend. Example: --backend-opts="cudnn(gpu=1)"`) 65 | parallel = flag.Int("parallelism", -1, "Number of games to play in parallel (-1 for default)") 66 | cacheDir = flag.String("cache", "", "Directory to use for downloaded files cache (if it exists)") 67 | useTestServer = flag.Bool("use-test-server", false, "Set host name to test server.") 68 | runId = flag.Uint("run", 0, "Which training run to contribute to (default 0 to let server decide)") 69 | keep = flag.Bool("keep", false, "Do not delete old network files") 70 | version = flag.Bool("version", false, "Print version and exit.") 71 | trainOnly = flag.Bool("train-only", false, "Do not play match games") 72 | report_host = flag.Bool("report-host", false, "Send hostname to server for more fine-grained statistics") 73 | report_gpu = flag.Bool("report-gpu", false, "Send gpu info to server for more fine-grained statistics") 74 | cudnn = flag.Bool("cudnn", true, "Prefer the cudnn backend (if available)") 75 | settingsPath = flag.String("config", "", "JSON configuration file to use") 76 | ) 77 | 78 | // Settings holds username and password. 79 | type Settings struct { 80 | User string 81 | Pass string 82 | Localhost string 83 | } 84 | 85 | const inf = "inf" 86 | 87 | /* 88 | Reads the user and password from a config file and returns empty strings if anything went wrong. 89 | */ 90 | func readSettings(path string) (string, string, string) { 91 | settings := Settings{} 92 | file, err := os.Open(path) 93 | if err != nil { 94 | // File was not found 95 | return "", "", "" 96 | } 97 | defer file.Close() 98 | decoder := json.NewDecoder(file) 99 | err = decoder.Decode(&settings) 100 | if err != nil { 101 | log.Fatal("Error decoding JSON ", err) 102 | return "", "", "" 103 | } 104 | return settings.User, settings.Pass, settings.Localhost 105 | } 106 | 107 | /* 108 | Prompts the user for a username and password and creates the config file. 109 | */ 110 | func createSettings(path string) (string, string) { 111 | settings := Settings{} 112 | 113 | fmt.Printf("Please enter your username and password, an account will be automatically created.\n") 114 | fmt.Printf("Note that this password will be stored in plain text, so avoid a password that is\n") 115 | fmt.Printf("also used for sensitive applications. It also cannot be recovered.\n") 116 | fmt.Printf("Enter username : ") 117 | fmt.Scanf("%s\n", &settings.User) 118 | fmt.Printf("Enter password : ") 119 | fmt.Scanf("%s\n", &settings.Pass) 120 | jsonSettings, err := json.Marshal(settings) 121 | if err != nil { 122 | log.Fatal("Cannot encode settings to JSON ", err) 123 | return "", "" 124 | } 125 | settingsFile, err := os.Create(path) 126 | defer settingsFile.Close() 127 | if err != nil { 128 | log.Fatal("Could not create output file ", err) 129 | return "", "" 130 | } 131 | settingsFile.Write(jsonSettings) 132 | return settings.User, settings.Pass 133 | } 134 | 135 | func getExtraParams() map[string]string { 136 | return map[string]string{ 137 | "user": *user, 138 | "password": *password, 139 | "version": "34", 140 | "token": strconv.Itoa(randId), 141 | "train_only": strconv.FormatBool(*trainOnly), 142 | "hostname": *localHost, 143 | "gpu": gpuType, 144 | "gpu_id": strconv.Itoa(*gpu), 145 | } 146 | } 147 | 148 | func uploadGame(httpClient *http.Client, path string, pgn string, 149 | nextGame client.NextGameResponse, version string, fp_threshold float64) error { 150 | 151 | var retryCount uint32 152 | 153 | for { 154 | retryCount++ 155 | if retryCount > 3 { 156 | return errors.New("UploadGame failed: Too many retries") 157 | } 158 | 159 | extraParams := getExtraParams() 160 | extraParams["training_id"] = strconv.Itoa(int(nextGame.TrainingId)) 161 | extraParams["network_id"] = strconv.Itoa(int(nextGame.NetworkId)) 162 | extraParams["pgn"] = pgn 163 | extraParams["engineVersion"] = version 164 | if fp_threshold >= 0.0 { 165 | extraParams["fp_threshold"] = strconv.FormatFloat(fp_threshold, 'E', -1, 64) 166 | } 167 | request, err := client.BuildUploadRequest(*hostname+"/upload_game", extraParams, "file", path) 168 | if err != nil { 169 | log.Printf("BUR: %v", err) 170 | return err 171 | } 172 | resp, err := httpClient.Do(request) 173 | if err != nil { 174 | log.Printf("http.Do: %v", err) 175 | return err 176 | } 177 | body := &bytes.Buffer{} 178 | _, err = body.ReadFrom(resp.Body) 179 | if err != nil { 180 | log.Print(err) 181 | log.Print("Error uploading, retrying...") 182 | time.Sleep(time.Second * (2 << retryCount)) 183 | continue 184 | } 185 | resp.Body.Close() 186 | if resp.StatusCode != 200 && strings.Contains(body.String(), " upgrade ") { 187 | log.Printf("The lc0 version you are using is not accepted by the server") 188 | if strings.Contains(version, "dev") { 189 | log.Printf("It is an unreleased development version") 190 | } else if strings.Contains(version, "rc") { 191 | log.Printf("It is a release candidate") 192 | } 193 | log.Printf("You probably need the latest release") 194 | os.Exit(5) 195 | } 196 | break 197 | } 198 | 199 | totalGames++ 200 | var duration = time.Since(startTime) 201 | var speed = int(float64(totalGames) / duration.Hours() * 24) 202 | log.Printf("Completed %d games in %s time (%d games/day)", totalGames, duration, speed) 203 | 204 | err := os.Remove(path) 205 | if err != nil { 206 | log.Printf("Failed to remove training file: %v", err) 207 | } 208 | 209 | return nil 210 | } 211 | 212 | type gameInfo struct { 213 | pgn string 214 | fname string 215 | // If >= 0, this is the value which if resign threshold was set 216 | // higher a false positive would have occurred if the game had been 217 | // played with resign. 218 | fp_threshold float64 219 | player1 string 220 | result string 221 | } 222 | 223 | type cmdWrapper struct { 224 | Cmd *exec.Cmd 225 | Pgn string 226 | Input io.WriteCloser 227 | BestMove chan string 228 | gi chan gameInfo 229 | Version string 230 | Retry chan bool 231 | } 232 | 233 | func (c *cmdWrapper) openInput() { 234 | var err error 235 | c.Input, err = c.Cmd.StdinPipe() 236 | if err != nil { 237 | log.Fatal(err) 238 | } 239 | } 240 | 241 | func convertMovesToPGN(moves []string, result string, start_ply_count int) string { 242 | game := chess.NewGame(chess.UseNotation(chess.LongAlgebraicNotation{})) 243 | if len(moves) > 6 && moves[len(moves)-7] == "from_fen" { 244 | fen := strings.Join(moves[len(moves)-6:], " ") 245 | moves = moves[:len(moves)-7] 246 | pair := &chess.TagPair{ 247 | Key: "FEN", 248 | Value: fen, 249 | } 250 | tagPairs := []*chess.TagPair{pair} 251 | fen_func, _ := chess.FEN(fen) 252 | game = chess.NewGame(chess.UseNotation(chess.LongAlgebraicNotation{}), fen_func, chess.TagPairs(tagPairs)) 253 | } 254 | for _, m := range moves { 255 | err := game.MoveStr(m) 256 | if err != nil { 257 | log.Fatalf("movstr: %v", err) 258 | } 259 | } 260 | if game.Outcome() == chess.NoOutcome && len(game.EligibleDraws()) > 1 { 261 | game.Draw(game.EligibleDraws()[1]) 262 | } 263 | game2 := chess.NewGame() 264 | b, err := game.MarshalText() 265 | if err != nil { 266 | log.Fatalf("MarshalText failed: %v", err) 267 | } 268 | b_str := string(b) 269 | if strings.HasSuffix(b_str, " *") && result != "" { 270 | to_append := "1/2-1/2" 271 | if result == "whitewon" { 272 | to_append = "1-0" 273 | } else if result == "blackwon" { 274 | to_append = "0-1" 275 | } 276 | b = []byte(strings.TrimRight(b_str, "*") + to_append) 277 | } 278 | game2.UnmarshalText(b) 279 | return game2.String() + " {OL: " + strconv.Itoa(start_ply_count) + "}" 280 | } 281 | 282 | func createCmdWrapper() *cmdWrapper { 283 | c := &cmdWrapper{ 284 | gi: make(chan gameInfo), 285 | BestMove: make(chan string), 286 | Version: "v0.10.0", 287 | Retry: make(chan bool), 288 | } 289 | return c 290 | } 291 | 292 | func checkLc0() { 293 | cmd := exec.Command(lc0Exe) 294 | cmd.Args = append(cmd.Args, "--help") 295 | out, err := cmd.CombinedOutput() 296 | if err != nil { 297 | log.Fatal(err) 298 | } 299 | if bytes.Contains(out, []byte("eigen")) { 300 | hasEigen = true 301 | } 302 | if bytes.Contains(out, []byte("dx12")) { 303 | hasDx = true 304 | } 305 | if bytes.Contains(out, []byte("cuda-auto")) { 306 | hasCuda = true 307 | parallelism32 = true 308 | } 309 | if bytes.Contains(out, []byte("cudnn-auto")) && *cudnn { 310 | hasCudnn = true 311 | parallelism32 = true 312 | } 313 | if bytes.Contains(out, []byte("opencl")) { 314 | hasOpenCL = true 315 | } 316 | } 317 | 318 | func checkDx(networkPath string) { 319 | if !hasEigen { 320 | log.Fatalf("Dx12 backend cannot be validated") 321 | } 322 | log.Println("Sanity checking the dx12 driver.") 323 | cmd := exec.Command(lc0Exe) 324 | sGpu := "" 325 | if *gpu >= 0 { 326 | sGpu = fmt.Sprintf(",gpu=%v", *gpu) 327 | } 328 | cmd.Args = append(cmd.Args, "benchmark", "-w", networkPath, "--backend=check") 329 | cmd.Args = append(cmd.Args, fmt.Sprintf("--backend-opts=mode=check,freq=1.0,atol=5e-1,dx12%v", sGpu)) 330 | // Add the startpos fen to get consistent behavior with old and new lc0 benchmark. 331 | cmd.Args = append(cmd.Args, "--fen=rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") 332 | out, err := cmd.CombinedOutput() 333 | if err != nil { 334 | log.Fatal(err) 335 | } 336 | if bytes.Contains(out, []byte("*** ERROR check failed")) { 337 | log.Fatal("The dx12 backend failed the self check - try updating gpu drivers") 338 | } 339 | log.Println("The dx12 driver passed the initial sanity check.") 340 | } 341 | 342 | func (c *cmdWrapper) launch(networkPath string, otherNetPath string, args []string, input bool) { 343 | c.Cmd = exec.Command(lc0Exe) 344 | // Add the "selfplay" or "uci" part first 345 | mode := args[0] 346 | c.Cmd.Args = append(c.Cmd.Args, mode) 347 | args = args[1:] 348 | if mode != "selfplay" { 349 | c.Cmd.Args = append(c.Cmd.Args, "--backend=multiplexing") 350 | } 351 | if *lc0Args != "" { 352 | log.Println("WARNING: Option --lc0args is for testing, not production use!") 353 | log.SetPrefix("TESTING: ") 354 | parts := strings.Split(*lc0Args, " ") 355 | c.Cmd.Args = append(c.Cmd.Args, parts...) 356 | } 357 | parallelism := *parallel 358 | sGpu := "" 359 | if *gpu >= 0 { 360 | sGpu = fmt.Sprintf(",gpu=%v", *gpu) 361 | } 362 | // Check the dx12 backend if it is the first time or we changed net, but only if no higher 363 | // priority backend is available. 364 | if !hasCuda && !hasCudnn && hasDx && testedDxNet != networkPath { 365 | checkDx(networkPath) 366 | testedDxNet = networkPath 367 | } 368 | if *backopts != "" { 369 | // Check against small token blacklist. 370 | tokens := regexp.MustCompile("[,=().0-9]").Split(*backopts, -1) 371 | for _, token := range tokens { 372 | switch token { 373 | case "mlh", "random", "recordreplay", "trivial": 374 | log.Fatalf("Not accepted in --backend-opts: %s", token) 375 | } 376 | } 377 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--backend-opts=%s", *backopts)) 378 | } else if hasCudnn { 379 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--backend-opts=backend=cudnn-auto%v", sGpu)) 380 | if parallelism <= 0 && parallelism32 { 381 | parallelism = 32 382 | } 383 | } else if hasCuda { 384 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--backend-opts=backend=cuda-auto%v", sGpu)) 385 | if parallelism <= 0 && parallelism32 { 386 | parallelism = 32 387 | } 388 | } else if hasDx { 389 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--backend-opts=check(freq=1e-5,atol=5e-1,dx12%v)", sGpu)) 390 | } else if hasOpenCL { 391 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--backend-opts=backend=opencl%v", sGpu)) 392 | } 393 | if parallelism > 0 && mode == "selfplay" { 394 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--parallelism=%v", parallelism)) 395 | } 396 | c.Cmd.Args = append(c.Cmd.Args, args...) 397 | if otherNetPath == "" { 398 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--weights=%s", networkPath)) 399 | } else { 400 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--player1.weights=%s", networkPath)) 401 | c.Cmd.Args = append(c.Cmd.Args, fmt.Sprintf("--player2.weights=%s", otherNetPath)) 402 | c.Cmd.Args = append(c.Cmd.Args, "--no-share-trees") 403 | } 404 | 405 | fmt.Printf("Args: %v\n", c.Cmd.Args) 406 | 407 | stdout, err := c.Cmd.StdoutPipe() 408 | if err != nil { 409 | log.Fatal(err) 410 | } 411 | 412 | c.Cmd.Stderr = c.Cmd.Stdout 413 | 414 | // If the game wasn't played with resign, and the engine supports it, 415 | // this will be populated by the resign_report before the gameready 416 | // with the value which the resign threshold should be kept below to 417 | // avoid a false positive. 418 | last_fp_threshold := -1.0 419 | go func() { 420 | defer close(c.BestMove) 421 | defer close(c.gi) 422 | stdoutScanner := bufio.NewScanner(stdout) 423 | for stdoutScanner.Scan() { 424 | line := stdoutScanner.Text() 425 | // fmt.Printf("lc0: %s\n", line) 426 | switch { 427 | case strings.HasPrefix(line, "Unknown command line flag"): 428 | fmt.Println(line) 429 | log.Fatal("You probably have an old lc0 version") 430 | case strings.Contains(line, "GPU: GeForce GTX 16"): 431 | fallthrough // Does not contain "fp16" so the following works fine. 432 | case strings.Contains(line, "Switching to"): 433 | fmt.Println(line) 434 | if parallelism == 32 && parallelism32 && !strings.Contains(line, "fp16") { 435 | parallelism32 = false 436 | if mode == "selfplay" && *parallel <= 0 { 437 | log.Println("Restarting with default parallelism") 438 | c.Retry <- true 439 | } 440 | } 441 | case strings.HasPrefix(line, "resign_report "): 442 | args := strings.Split(line, " ") 443 | fp_threshold_idx := -1 444 | for idx, arg := range args { 445 | if arg == "fp_threshold" { 446 | fp_threshold_idx = idx + 1 447 | } 448 | } 449 | if fp_threshold_idx >= 0 { 450 | last_fp_threshold, err = strconv.ParseFloat(args[fp_threshold_idx], 64) 451 | if err != nil { 452 | log.Printf("Malformed resign_report: %q", line) 453 | last_fp_threshold = -1.0 454 | } 455 | } 456 | fmt.Println(line) 457 | case strings.HasPrefix(line, "gameready "): 458 | // filename is between "trainingfile" and "gameid" 459 | idx1 := strings.Index(line, "trainingfile") 460 | idx2 := strings.LastIndex(line, "gameid") 461 | idx3 := strings.LastIndex(line, "moves") 462 | if idx1 < 0 || idx2 < 0 || idx3 < 0 { 463 | log.Printf("Malformed gameready: %q", line) 464 | break 465 | } 466 | idx4 := strings.LastIndex(line, "player1") 467 | idx5 := strings.LastIndex(line, "result") 468 | idx6 := strings.LastIndex(line, "play_start_ply") 469 | result := "" 470 | if idx5 < 0 { 471 | idx5 = idx3 472 | } else { 473 | result = line[idx5+7 : idx3-1] 474 | } 475 | player := "" 476 | if idx4 >= 0 { 477 | player = line[idx4+8 : idx5-1] 478 | } 479 | start_ply_count := -1 480 | if idx6 >= 0 { 481 | start_ply_count, err = strconv.Atoi(line[idx6+15 : idx4-1]) 482 | } 483 | file := line[idx1+13 : idx2-1] 484 | pgn := convertMovesToPGN(strings.Split(line[idx3+6:len(line)], " "), result, start_ply_count) 485 | fmt.Printf("PGN: %s\n", pgn) 486 | c.gi <- gameInfo{pgn: pgn, fname: file, fp_threshold: last_fp_threshold, player1: player, result: result} 487 | last_fp_threshold = -1.0 488 | case strings.HasPrefix(line, "bestmove "): 489 | // fmt.Println(line) 490 | c.BestMove <- strings.Split(line, " ")[1] 491 | case strings.HasPrefix(line, "id name Lc0 "): 492 | c.Version = strings.Split(line, " ")[3] 493 | fmt.Println(line) 494 | case strings.HasPrefix(line, "info"): 495 | break 496 | case strings.HasPrefix(line, "GPU: "): 497 | if *report_gpu && *backopts == "" { 498 | gpuType = strings.TrimPrefix(line, "GPU: ") 499 | } 500 | fmt.Println(line) 501 | case strings.HasPrefix(line, "Selected device: "): 502 | if *report_gpu && *backopts == "" { 503 | gpuType = strings.TrimPrefix(line, "Selected device: ") 504 | } 505 | fmt.Println(line) 506 | case strings.HasPrefix(line, "BLAS"): 507 | if *report_gpu && *backopts == "" { 508 | gpuType = "None" 509 | } 510 | fmt.Println(line) 511 | case strings.HasPrefix(line, "*** ERROR check failed"): 512 | fmt.Println(line) 513 | log.Fatal("The dx12 backend failed the self check - try updating gpu drivers") 514 | default: 515 | fmt.Println(line) 516 | } 517 | } 518 | }() 519 | 520 | if input { 521 | c.openInput() 522 | } 523 | 524 | err = c.Cmd.Start() 525 | if err != nil { 526 | log.Fatal(err) 527 | } 528 | } 529 | 530 | func resultToNum(result string) int { 531 | if result == "whitewon" { 532 | return 1 533 | } 534 | if result == "blackwon" { 535 | return -1 536 | } 537 | return 0 538 | } 539 | 540 | func playMatch(httpClient *http.Client, ngr client.NextGameResponse, baselinePath string, candidatePath string, params []string) (*client.NextGameResponse, error) { 541 | // lc0 needs selfplay first in the argument list. 542 | params = append([]string{"selfplay"}, params...) 543 | // Training flag used for simplicity for now. 544 | params = append(params, "--training=true") 545 | hasVisitsParam := false 546 | for i := range params { 547 | if strings.HasPrefix(params[i], "--visits=") || strings.HasPrefix(params[i], "--playouts=") { 548 | hasVisitsParam = true 549 | } 550 | } 551 | if !hasVisitsParam { 552 | params = append(params, "--visits=800") 553 | } 554 | c := createCmdWrapper() 555 | c.launch(candidatePath, baselinePath, params /* input= */, false) 556 | trainDirHolder := make([]string, 1) 557 | trainDirHolder[0] = "" 558 | defer func() { 559 | // Remove the training dir when we're done training. 560 | trainDir := trainDirHolder[0] 561 | if trainDir != "" { 562 | log.Printf("Removing traindir: %s", trainDir) 563 | err := os.RemoveAll(trainDir) 564 | if err != nil { 565 | log.Printf("Error removing train dir: %v", err) 566 | } 567 | } 568 | }() 569 | doneCh := make(chan bool) 570 | gameInfoCh := make(chan gameInfo) 571 | reverseDoneCh := make(chan bool) 572 | wg := &sync.WaitGroup{} 573 | wg.Add(1) 574 | var pendingNextGame *client.NextGameResponse 575 | go func() { 576 | defer wg.Done() 577 | defer close(doneCh) 578 | errCount := 0 579 | curng := &ngr 580 | var flipped []gameInfo 581 | var normal []gameInfo 582 | for done := false; !done; { 583 | select { 584 | case <-reverseDoneCh: 585 | log.Println("Match uploader exiting") 586 | return 587 | case gi, _ := <-gameInfoCh: 588 | if gi.player1 == "black" { 589 | flipped = append(flipped, gi) 590 | } else { 591 | normal = append(normal, gi) 592 | } 593 | for true { 594 | if curng != nil { 595 | if curng.Flip && len(flipped) > 0 { 596 | l := len(flipped) 597 | nextgi := flipped[l-1] 598 | flipped = flipped[:l-1] 599 | log.Println("uploading match result") 600 | extraParams := getExtraParams() 601 | extraParams["engineVersion"] = c.Version 602 | client.UploadMatchResult(httpClient, *hostname, curng.MatchGameId, -resultToNum(nextgi.result), nextgi.pgn, extraParams) 603 | log.Println("uploaded") 604 | curng = nil 605 | } else if !curng.Flip && len(normal) > 0 { 606 | l := len(normal) 607 | nextgi := normal[l-1] 608 | normal = normal[:l-1] 609 | log.Println("uploading match result") 610 | extraParams := getExtraParams() 611 | extraParams["engineVersion"] = c.Version 612 | client.UploadMatchResult(httpClient, *hostname, curng.MatchGameId, resultToNum(nextgi.result), nextgi.pgn, extraParams) 613 | log.Println("uploaded") 614 | curng = nil 615 | } 616 | } 617 | if curng != nil { 618 | break 619 | } 620 | ng, err := client.NextGame(httpClient, *hostname, getExtraParams()) 621 | if err != nil { 622 | fmt.Printf("Error talking to server: %v\n", err) 623 | errCount++ 624 | if errCount < 10 { 625 | break 626 | } 627 | return 628 | } 629 | if ng.Type != ngr.Type || ng.Sha != ngr.Sha || ng.CandidateSha != ngr.CandidateSha { 630 | log.Println("Current match finished.") 631 | pendingNextGame = &ng 632 | return 633 | } 634 | curng = &ng 635 | errCount = 0 636 | } 637 | } 638 | } 639 | }() 640 | progressOrKill := false 641 | for done := false; !done; { 642 | select { 643 | case <-c.Retry: 644 | close(reverseDoneCh) 645 | return nil, errors.New("retry") 646 | case <-doneCh: 647 | done = true 648 | progressOrKill = true 649 | log.Println("Received message to end matches, killing lc0") 650 | c.Cmd.Process.Kill() 651 | case _, ok := <-c.BestMove: 652 | // Just swallow the best moves, not actually needed. 653 | if !ok { 654 | log.Printf("BestMove channel closed unexpectedly, exiting match loop") 655 | break 656 | } 657 | case gi, ok := <-c.gi: 658 | if !ok { 659 | log.Printf("GameInfo channel closed, exiting match loop") 660 | done = true 661 | break 662 | } 663 | progressOrKill = true 664 | trainDirHolder[0] = path.Dir(gi.fname) 665 | wg.Add(1) 666 | go func() { 667 | select { 668 | case <-doneCh: 669 | case gameInfoCh <- gi: 670 | } 671 | wg.Done() 672 | }() 673 | } 674 | } 675 | 676 | log.Println("Waiting for lc0 to stop") 677 | err := c.Cmd.Wait() 678 | if err != nil { 679 | fmt.Printf("lc0 exited with: %v", err) 680 | } 681 | log.Println("lc0 stopped") 682 | close(reverseDoneCh) 683 | 684 | log.Println("Waiting for uploads to complete") 685 | wg.Wait() 686 | if !progressOrKill { 687 | return nil, errors.New("Client self-exited without producing any matches.") 688 | } 689 | return pendingNextGame, nil 690 | } 691 | 692 | func train(httpClient *http.Client, ngr client.NextGameResponse, 693 | networkPath string, otherNetPath string, count int, params []string, doneCh chan bool) error { 694 | // lc0 needs selfplay first in the argument list. 695 | params = append([]string{"selfplay"}, params...) 696 | params = append(params, "--training=true") 697 | c := createCmdWrapper() 698 | c.launch(networkPath, otherNetPath, params /* input= */, false) 699 | trainDirHolder := make([]string, 1) 700 | trainDirHolder[0] = "" 701 | defer func() { 702 | // Remove the training dir when we're done training. 703 | trainDir := trainDirHolder[0] 704 | if trainDir != "" { 705 | log.Printf("Removing traindir: %s", trainDir) 706 | err := os.RemoveAll(trainDir) 707 | if err != nil { 708 | log.Printf("Error removing train dir: %v", err) 709 | } 710 | } 711 | }() 712 | wg := &sync.WaitGroup{} 713 | numGames := 1 714 | progressOrKill := false 715 | for done := false; !done; { 716 | select { 717 | case <-c.Retry: 718 | return errors.New("retry") 719 | case <-doneCh: 720 | done = true 721 | progressOrKill = true 722 | log.Println("Received message to end training, killing lc0") 723 | c.Cmd.Process.Kill() 724 | case _, ok := <-c.BestMove: 725 | // Just swallow the best moves, only needed for match play. 726 | if !ok { 727 | log.Printf("BestMove channel closed unexpectedly, exiting train loop") 728 | break 729 | } 730 | case gi, ok := <-c.gi: 731 | if !ok { 732 | log.Printf("GameInfo channel closed, exiting train loop") 733 | done = true 734 | break 735 | } 736 | fmt.Printf("Uploading game: %d\n", numGames) 737 | numGames++ 738 | progressOrKill = true 739 | trainDirHolder[0] = path.Dir(gi.fname) 740 | log.Printf("trainDir=%s", trainDirHolder[0]) 741 | wg.Add(1) 742 | go func() { 743 | uploadGame(httpClient, gi.fname, gi.pgn, ngr, c.Version, gi.fp_threshold) 744 | wg.Done() 745 | }() 746 | } 747 | } 748 | 749 | log.Println("Waiting for lc0 to stop") 750 | err := c.Cmd.Wait() 751 | if err != nil { 752 | fmt.Printf("lc0 exited with: %v", err) 753 | } 754 | log.Println("lc0 stopped") 755 | 756 | log.Println("Waiting for uploads to complete") 757 | wg.Wait() 758 | if !progressOrKill { 759 | return errors.New("Client self-exited without producing any games.") 760 | } 761 | return nil 762 | } 763 | 764 | func checkValidNetwork(dir string, sha string) (string, error) { 765 | // Sha already exists? 766 | path := filepath.Join(dir, sha) 767 | _, err := os.Stat(path) 768 | if err == nil { 769 | file, _ := os.Open(path) 770 | reader, err := gzip.NewReader(file) 771 | if err == nil { 772 | var bytes []byte 773 | bytes, err = ioutil.ReadAll(reader) 774 | sum := sha256.Sum256(bytes) 775 | got := fmt.Sprintf("%x", sum) 776 | if sha != got { 777 | text := fmt.Sprintf("sha mismatch want:\n%s\ngot\n%s\n", sha, got) 778 | err = errors.New(text) 779 | } 780 | } 781 | file.Close() 782 | if err != nil { 783 | fmt.Printf("Deleting invalid network...\n") 784 | os.Remove(path) 785 | return path, err 786 | } else { 787 | return path, nil 788 | } 789 | } 790 | return path, err 791 | } 792 | 793 | func removeAllExcept(dir string, sha string, keepTime string) error { 794 | files, err := ioutil.ReadDir(dir) 795 | if err != nil { 796 | return err 797 | } 798 | for _, file := range files { 799 | if file.Name() == sha { 800 | continue 801 | } 802 | timeLimit, _ := time.ParseDuration(keepTime) 803 | if time.Since(file.ModTime()) < timeLimit { 804 | continue 805 | } 806 | fmt.Printf("Removing %v\n", file.Name()) 807 | err := os.RemoveAll(filepath.Join(dir, file.Name())) 808 | if err != nil { 809 | return err 810 | } 811 | } 812 | return nil 813 | } 814 | 815 | func acquireLock(dir string, sha string) (*flock.Flock, bool, error) { 816 | lockpath, _ := filepath.Abs(filepath.Join(dir, sha+".lck")) 817 | lock := flock.New(lockpath) 818 | // Attempt to acquire lock 819 | success, err := lock.TryLock() 820 | return lock, success, err 821 | } 822 | 823 | func makeCacheDir(dir string) string { 824 | userCache := *cacheDir 825 | if len(userCache) == 0 { 826 | if runtime.GOOS == "linux" { 827 | userCache = os.Getenv("XDG_CACHE_HOME") 828 | if len(userCache) == 0 { 829 | homeDir := os.Getenv("HOME") 830 | if len(homeDir) != 0 { 831 | userCache = homeDir + "/.cache" 832 | } 833 | } 834 | } else if runtime.GOOS == "darwin" { 835 | homeDir := os.Getenv("HOME") 836 | if len(homeDir) != 0 { 837 | userCache = homeDir + "/Library/Caches" 838 | } 839 | } 840 | } 841 | if len(userCache) != 0 { 842 | _, err := os.Stat(userCache) 843 | if err == nil { 844 | if len(*cacheDir) == 0 { 845 | userCache = filepath.Join(userCache, "lc0") 846 | } 847 | dir = filepath.Join(userCache, dir) 848 | } 849 | } 850 | os.MkdirAll(dir, os.ModePerm) 851 | return dir 852 | } 853 | 854 | func getNetwork(httpClient *http.Client, sha string, keepTime string) (string, error) { 855 | dir := makeCacheDir("client-cache") 856 | if keepTime != inf { 857 | err := removeAllExcept(dir, sha, keepTime) 858 | if err != nil { 859 | log.Printf("Failed to remove old network(s): %v", err) 860 | } 861 | } 862 | path, err := checkValidNetwork(dir, sha) 863 | if err == nil { 864 | // There is already a valid network. Use it. 865 | return path, nil 866 | } 867 | 868 | // Otherwise, let's download it 869 | lock, lockHeld, err := acquireLock(dir, sha) 870 | 871 | if err != nil || !lockHeld { 872 | if !lockHeld { 873 | log.Println("Download initiated by other client - waiting") 874 | for i := 0; i < 60; i++ { 875 | time.Sleep(time.Second) 876 | path, err := checkValidNetwork(dir, sha) 877 | if err == nil { 878 | return path, nil 879 | } 880 | } 881 | return "", errors.New("Timed out") 882 | } else { 883 | log.Fatalf("Unable to lock: %v", err) 884 | } 885 | } 886 | 887 | // Lockfile acquired, download it 888 | defer lock.Unlock() 889 | fmt.Println("Downloading network...") 890 | for i := 0; i < 3; i++ { 891 | if i > 0 { 892 | log.Println("Waiting 10 seconds before retrying") 893 | time.Sleep(10 * time.Second) 894 | } 895 | err = client.DownloadNetwork(httpClient, *networkMirror, path, sha) 896 | if err == nil { 897 | return checkValidNetwork(dir, sha) 898 | } 899 | log.Printf("Network download failed: %v", err) 900 | } 901 | return "", err 902 | } 903 | 904 | func checkValidBook(path string, sha string) (string, error) { 905 | // File already exists? 906 | _, err := os.Stat(path) 907 | if err == nil { 908 | file, _ := os.Open(path) 909 | sum := sha256.New() 910 | _, err := io.Copy(sum, file) 911 | got := fmt.Sprintf("%x", sum.Sum(nil)) 912 | if sha != got { 913 | text := fmt.Sprintf("book sha mismatch want:\n%s\ngot\n%s\n", sha, got) 914 | err = errors.New(text) 915 | } 916 | file.Close() 917 | if err != nil { 918 | fmt.Printf("Deleting invalid book...\n") 919 | os.Remove(path) 920 | return path, err 921 | } else { 922 | return path, nil 923 | } 924 | } 925 | return path, err 926 | } 927 | 928 | func getBook(httpClient *http.Client, book_url string, sha string) (string, error) { 929 | dir := makeCacheDir("books") 930 | u, err := url.Parse(book_url) 931 | if err != nil { 932 | log.Println("Unable to parse book URL") 933 | return "", err 934 | } 935 | s := strings.Split(u.Path, "/") 936 | book_name := s[len(s)-1] 937 | path := filepath.Join(dir, book_name) 938 | _, err = checkValidBook(path, sha) 939 | if err == nil { 940 | // Book is there, use it. 941 | return path, nil 942 | } 943 | 944 | // Otherwise, let's download it 945 | lock, lockHeld, err := acquireLock(dir, book_name) 946 | 947 | if err != nil || !lockHeld { 948 | if !lockHeld { 949 | log.Println("Book download initiated by other client") 950 | return "", err 951 | } else { 952 | log.Fatalf("Unable to lock: %v", err) 953 | } 954 | } 955 | 956 | // Lockfile acquired, download it 957 | defer lock.Unlock() 958 | fmt.Println("Downloading book...") 959 | 960 | r, err := httpClient.Get(book_url) 961 | if err != nil { 962 | log.Println("Book download failed") 963 | return "", err 964 | } 965 | 966 | out, err := ioutil.TempFile(dir, book_name+"_tmp") 967 | if err != nil { 968 | log.Println("Unable to create temporary file") 969 | return "", err 970 | } 971 | 972 | _, err = io.Copy(out, r.Body) 973 | r.Body.Close() 974 | out.Close() 975 | if err == nil { 976 | err = os.Rename(out.Name(), path) 977 | } 978 | // Ensure tmpfile is erased 979 | os.Remove(out.Name()) 980 | 981 | return checkValidBook(path, sha) 982 | } 983 | 984 | func nextGame(httpClient *http.Client, count int) error { 985 | var nextGame client.NextGameResponse 986 | var err error 987 | if pendingNextGame != nil { 988 | nextGame = *pendingNextGame 989 | pendingNextGame = nil 990 | err = nil 991 | } else { 992 | nextGame, err = client.NextGame(httpClient, *hostname, getExtraParams()) 993 | if err != nil { 994 | return err 995 | } 996 | } 997 | var serverParams []string 998 | err = json.Unmarshal([]byte(nextGame.Params), &serverParams) 999 | if err != nil { 1000 | return err 1001 | } 1002 | log.Printf("serverParams: %s", serverParams) 1003 | 1004 | if nextGame.BookUrl != "" { 1005 | book, err := getBook(&http.Client{}, nextGame.BookUrl, nextGame.BookSha) 1006 | if err != nil { 1007 | return err 1008 | } 1009 | // Replace the book file with the correct path 1010 | for i := range serverParams { 1011 | if strings.HasPrefix(serverParams[i], "--openings-pgn=") { 1012 | serverParams[i] = "--openings-pgn=" + book 1013 | break 1014 | } 1015 | } 1016 | } 1017 | 1018 | if nextGame.Type == "match" { 1019 | log.Println("Getting networks for match") 1020 | networkPath, err := getNetwork(httpClient, nextGame.Sha, inf) 1021 | if err != nil { 1022 | return err 1023 | } 1024 | candidatePath, err := getNetwork(httpClient, nextGame.CandidateSha, inf) 1025 | if err != nil { 1026 | return err 1027 | } 1028 | log.Println("Starting match") 1029 | possibleNextGame, err := playMatch(httpClient, nextGame, networkPath, candidatePath, serverParams) 1030 | if err != nil { 1031 | log.Printf("playMatch: %v", err) 1032 | return err 1033 | } 1034 | pendingNextGame = possibleNextGame 1035 | return nil 1036 | } 1037 | 1038 | if nextGame.Type == "train" { 1039 | keepTime := nextGame.KeepTime 1040 | if *keep { 1041 | keepTime = inf 1042 | } else if keepTime == "" { 1043 | // Four hours should be enough for clients serving 2 parallel runs in 1044 | // the same directory, even after one or two failed failed promotions. 1045 | keepTime = "4h" 1046 | } 1047 | networkPath, err := getNetwork(httpClient, nextGame.Sha, keepTime) 1048 | if err != nil { 1049 | return err 1050 | } 1051 | otherNetPath := "" 1052 | if nextGame.CandidateSha != "" { 1053 | otherNetPath, err = getNetwork(httpClient, nextGame.CandidateSha, inf) 1054 | if err != nil { 1055 | return err 1056 | } 1057 | } 1058 | doneCh := make(chan bool) 1059 | go func() { 1060 | defer close(doneCh) 1061 | errCount := 0 1062 | for { 1063 | time.Sleep(60 * time.Second) 1064 | if nextGame.Type == "Done" { 1065 | return 1066 | } 1067 | ng, err := client.NextGame(httpClient, *hostname, getExtraParams()) 1068 | if err != nil { 1069 | fmt.Printf("Error talking to server: %v\n", err) 1070 | errCount++ 1071 | if errCount < 10 { 1072 | continue 1073 | } 1074 | return 1075 | } 1076 | if ng.Type != nextGame.Type || ng.Sha != nextGame.Sha { 1077 | // Prefetch the next net before terminating game. 1078 | if ng.Type == "match" { 1079 | getNetwork(httpClient, ng.CandidateSha, inf) 1080 | } else { 1081 | getNetwork(httpClient, ng.Sha, inf) 1082 | } 1083 | pendingNextGame = &ng 1084 | return 1085 | } 1086 | errCount = 0 1087 | } 1088 | }() 1089 | err = train(httpClient, nextGame, networkPath, otherNetPath, count, serverParams, doneCh) 1090 | // Ensure the anonymous function stops retrying. 1091 | nextGame.Type = "Done" 1092 | if err != nil { 1093 | return err 1094 | } 1095 | return nil 1096 | } 1097 | 1098 | return errors.New("Unknown game type: " + nextGame.Type) 1099 | } 1100 | 1101 | // Ensure Tilps/chess is new enough. 1102 | func testChessVersion() { 1103 | if chess.GetLibraryVersion() < 3 { 1104 | log.Fatal("You need a more recent version of package github.com/Tilps/chess") 1105 | } 1106 | } 1107 | 1108 | func hideLc0argsFlag() { 1109 | shown := new(flag.FlagSet) 1110 | flag.VisitAll(func(f *flag.Flag) { 1111 | if f.Name != "lc0args" { 1112 | shown.Var(f.Value, f.Name, f.Usage) 1113 | } 1114 | }) 1115 | flag.Usage = func() { 1116 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 1117 | shown.PrintDefaults() 1118 | } 1119 | } 1120 | 1121 | func maybeSetTrainOnly() { 1122 | found := false 1123 | flag.Visit(func(f *flag.Flag) { 1124 | if f.Name == "train-only" { 1125 | found = true 1126 | } 1127 | }) 1128 | if !found && !hasCudnn && !hasCuda && !hasDx { 1129 | *trainOnly = true 1130 | log.Println("Will only run training games, use -train-only=false to override") 1131 | } 1132 | } 1133 | 1134 | func main() { 1135 | fmt.Printf("Lc0 client version %v\n", getExtraParams()["version"]) 1136 | 1137 | testChessVersion() 1138 | 1139 | hideLc0argsFlag() 1140 | flag.Parse() 1141 | 1142 | if *version { 1143 | return 1144 | } 1145 | 1146 | if runtime.GOOS == "windows" { 1147 | lc0Exe = "lc0.exe" 1148 | } 1149 | dir, _ := os.Getwd() 1150 | fi, err := os.Stat(path.Join(dir, lc0Exe)) 1151 | if err == nil && !fi.Mode().IsDir() { 1152 | lc0Exe = path.Join(dir, lc0Exe) 1153 | } 1154 | checkLc0() 1155 | 1156 | maybeSetTrainOnly() 1157 | 1158 | // 640 ought to be enough for anybody. 1159 | if *runId > 640 { 1160 | log.Fatal("Training run number too large") 1161 | } 1162 | randBytes := make([]byte, 2) 1163 | _, err = rand.Reader.Read(randBytes) 1164 | if err != nil { 1165 | randId = -1 1166 | } else { 1167 | randId = int(*runId)<<16 | int(randBytes[0])<<8 | int(randBytes[1]) 1168 | } 1169 | 1170 | if *useTestServer { 1171 | *hostname = "http://testserver.lczero.org" 1172 | } 1173 | 1174 | if len(*networkMirror) == 0 { 1175 | *networkMirror = *hostname + "/get_network?sha=" 1176 | } 1177 | 1178 | log.SetFlags(log.LstdFlags | log.Lshortfile) 1179 | 1180 | if len(*settingsPath) == 0 { 1181 | *settingsPath = "lc0-training-client-config.json" 1182 | configDir := "" 1183 | if runtime.GOOS == "linux" { 1184 | configDir = os.Getenv("XDG_CONFIG_HOME") 1185 | if len(configDir) == 0 { 1186 | homeDir := os.Getenv("HOME") 1187 | if len(homeDir) != 0 { 1188 | configDir = homeDir + "/.config" 1189 | } 1190 | } 1191 | } else if runtime.GOOS == "darwin" { 1192 | homeDir := os.Getenv("HOME") 1193 | if len(homeDir) != 0 { 1194 | configDir = homeDir + "/Library/Preferences" 1195 | } 1196 | } 1197 | 1198 | if len(configDir) != 0 { 1199 | configDir = filepath.Join(configDir, "lc0") 1200 | _, err = os.Stat(configDir) 1201 | if os.IsNotExist(err) { 1202 | err = os.Mkdir(configDir, os.ModePerm) 1203 | } 1204 | if err == nil { 1205 | *settingsPath = filepath.Join(configDir, *settingsPath) 1206 | } 1207 | } 1208 | } 1209 | 1210 | settingsUser, settingsPassword, settingsHost := readSettings(*settingsPath) 1211 | if len(*user) == 0 || len(*password) == 0 { 1212 | *user = settingsUser 1213 | *password = settingsPassword 1214 | 1215 | if len(*user) == 0 || len(*password) == 0 { 1216 | *user, *password = createSettings(*settingsPath) 1217 | } 1218 | } 1219 | 1220 | if len(settingsHost) != 0 && len(*localHost) == 0 { 1221 | *localHost = settingsHost 1222 | } 1223 | 1224 | if len(*user) == 0 { 1225 | log.Fatal("You must specify a username") 1226 | } 1227 | if len(*password) == 0 { 1228 | log.Fatal("You must specify a non-empty password") 1229 | } 1230 | 1231 | if *report_host && len(*localHost) == 0 { 1232 | s, err := os.Hostname() 1233 | if err == nil { 1234 | *localHost = s 1235 | } 1236 | } 1237 | 1238 | if len(*localHost) == 0 { 1239 | *localHost = defaultLocalHost 1240 | } 1241 | 1242 | httpClient := &http.Client{Timeout: 300 * time.Second} 1243 | startTime = time.Now() 1244 | for i := 0; ; i++ { 1245 | err := nextGame(httpClient, i) 1246 | if err != nil { 1247 | if err.Error() == "retry" { 1248 | time.Sleep(1 * time.Second) 1249 | continue 1250 | } 1251 | log.Print(err) 1252 | log.Print("Sleeping for 30 seconds...") 1253 | time.Sleep(30 * time.Second) 1254 | continue 1255 | } 1256 | } 1257 | } 1258 | -------------------------------------------------------------------------------- /src/client/client_http.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "mime/multipart" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | func postParams(httpClient *http.Client, uri string, data map[string]string, target interface{}) error { 20 | var encoded string 21 | if data != nil { 22 | values := url.Values{} 23 | for key, val := range data { 24 | values.Set(key, val) 25 | } 26 | encoded = values.Encode() 27 | } 28 | r, err := httpClient.Post(uri, "application/x-www-form-urlencoded", strings.NewReader(encoded)) 29 | if err != nil { 30 | return err 31 | } 32 | defer r.Body.Close() 33 | b, _ := ioutil.ReadAll(r.Body) 34 | if target != nil { 35 | err = json.Unmarshal(b, target) 36 | if err != nil { 37 | if strings.Contains(string(b), " upgrade ") { 38 | log.Printf("The client version you are using is not accepted by the server") 39 | os.Exit(5) 40 | } 41 | log.Printf("Bad JSON from %s -- %s\n", uri, string(b)) 42 | } 43 | } 44 | return err 45 | } 46 | 47 | // Creates a new file upload http request with optional extra params 48 | func BuildUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) { 49 | file, err := os.Open(path) 50 | if err != nil { 51 | return nil, err 52 | } 53 | defer file.Close() 54 | 55 | body := &bytes.Buffer{} 56 | writer := multipart.NewWriter(body) 57 | part, err := writer.CreateFormFile(paramName, filepath.Base(path)) 58 | if err != nil { 59 | return nil, err 60 | } 61 | _, err = io.Copy(part, file) 62 | 63 | for key, val := range params { 64 | _ = writer.WriteField(key, val) 65 | } 66 | err = writer.Close() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | req, err := http.NewRequest("POST", uri, body) 72 | if err != nil { 73 | return nil, err 74 | } 75 | req.Header.Set("Content-Type", writer.FormDataContentType()) 76 | return req, err 77 | } 78 | 79 | type NextGameResponse struct { 80 | Type string 81 | TrainingId uint 82 | NetworkId uint 83 | Sha string 84 | CandidateSha string 85 | Params string 86 | Flip bool 87 | MatchGameId uint 88 | KeepTime string 89 | BookUrl string 90 | BookSha string 91 | } 92 | 93 | func NextGame(httpClient *http.Client, hostname string, params map[string]string) (NextGameResponse, error) { 94 | resp := NextGameResponse{} 95 | err := postParams(httpClient, hostname+"/next_game", params, &resp) 96 | 97 | if len(resp.Sha) == 0 { 98 | return resp, errors.New("Server gave back empty SHA") 99 | } 100 | 101 | return resp, err 102 | } 103 | 104 | func UploadMatchResult(httpClient *http.Client, hostname string, match_game_id uint, result int, pgn string, params map[string]string) error { 105 | params["match_game_id"] = strconv.Itoa(int(match_game_id)) 106 | params["result"] = strconv.Itoa(result) 107 | params["pgn"] = pgn 108 | return postParams(httpClient, hostname+"/match_result", params, nil) 109 | } 110 | 111 | func DownloadNetwork(httpClient *http.Client, uriPrefix string, networkPath string, sha string) error { 112 | uri := uriPrefix + sha 113 | r, err := httpClient.Get(uri) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | if r.StatusCode >= 400 { 119 | return errors.New("Network server gave error status.") 120 | } 121 | 122 | dir, _ := filepath.Split(networkPath) 123 | out, err := ioutil.TempFile(dir, sha+"_tmp") 124 | if err != nil { 125 | return err 126 | } 127 | 128 | _, err = io.Copy(out, r.Body) 129 | r.Body.Close() 130 | out.Close() 131 | if err == nil { 132 | err = os.Rename(out.Name(), networkPath) 133 | } 134 | // Ensure tmpfile is erased 135 | os.Remove(out.Name()) 136 | return err 137 | } 138 | --------------------------------------------------------------------------------