├── .gitattributes ├── LICENSE ├── README.md ├── docker ├── Dockerfile ├── build.sh └── run.sh ├── exps ├── parity.py └── sudoku.py ├── images ├── forward_pass.png ├── mnist_sudoku.png └── poster_forward.png ├── notebooks └── Learning and Solving Sudoku via SATNet.ipynb ├── requirements.txt ├── satnet ├── __init__.py └── models.py ├── setup.py └── src ├── satnet.cpp ├── satnet.h ├── satnet_cpu.cpp └── satnet_cuda.cu /.gitattributes: -------------------------------------------------------------------------------- 1 | notebooks/*.ipynb linguist-documentation 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Po-Wei Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SATNet • [![PyPi][pypi-image]][pypi] [![colab][colab-image]][colab] [![License][license-image]][license] 2 | 3 | [license-image]: https://img.shields.io/badge/License-MIT-yellow.svg 4 | [license]: LICENSE 5 | 6 | [pypi-image]: https://img.shields.io/pypi/v/satnet.svg 7 | [pypi]: https://pypi.python.org/pypi/satnet 8 | 9 | [colab-image]: https://colab.research.google.com/assets/colab-badge.svg 10 | [colab]: https://colab.research.google.com/drive/1dRfepPLEE8N6BBZhXz8bbLDcPnRKaOcJ#forceEdit=true&offline=true&sandboxMode=true 11 | 12 | *Bridging deep learning and logical reasoning using a differentiable satisfiability solver.* 13 | 14 | This repository contains the source code to reproduce the experiments in the ICML 2019 paper [SATNet: Bridging deep learning and logical reasoning using a differentiable satisfiability solver](https://arxiv.org/abs/1905.12149) by [Po-Wei Wang](https://powei.tw/), [Priya L. Donti](https://priyadonti.com/), [Bryan Wilder](http://teamcore.usc.edu/people/bryanwilder/default.htm), and [J. Zico Kolter](http://zicokolter.com/). 15 | 16 | 17 | ## What is SATNet 18 | 19 | SATNet is a differentiable (smoothed) maximum satisfiability (MAXSAT) solver that can be integrated into the loop of larger deep learning systems. This (approximate) solver is based upon a fast coordinate descent approach to solving the semidefinite program (SDP) associated with the MAXSAT problem. 20 | 21 | #### How SATNet works 22 | 23 | A SATNet layer takes as input the discrete or probabilistic assignments of known MAXSAT variables, and outputs guesses for the assignments of unknown variables via a MAXSAT SDP relaxation with weights *S*. A schematic depicting the forward pass of this layer is shown below. To obtain the backward pass, we analytically differentiate through the SDP relaxation (see the paper for more details). 24 | 25 | ![Forward pass](images/poster_forward.png) 26 | 27 | #### Overview of experiments 28 | 29 | We show that by integrating SATNet into end-to-end learning systems, we can learn the logical structure of challenging problems in a minimally supervised fashion. In particular, we show that we can: 30 | * Learn the **parity function** using single-bit supervision (a traditionally hard task for deep networks) 31 | * Learn how to play **9×9 Sudoku (original and permuted)** solely from examples. 32 | * Solve a **"visual Sudoku"** problem that maps images of Sudoku puzzles to their associated logical solutions. (A sample "visual Sudoku" input is shown below.) 33 | 34 |
35 | 36 | 37 | 38 | ## Installation 39 | 40 | ### Via pip 41 | ```bash 42 | pip install satnet 43 | ``` 44 | 45 | 46 | ### From source 47 | ```bash 48 | git clone https://github.com/locuslab/SATNet 49 | cd SATNet && python setup.py install 50 | ``` 51 | 52 | #### Package Dependencies 53 | ``` 54 | conda install -c pytorch tqdm 55 | ``` 56 | The package also depends on the nvcc compiler. If it doesn't exist (try nvcc from commandline), you can install it via 57 | ``` 58 | conda install -c conda-forge cudatoolkit-dev 59 | ``` 60 | 61 | 62 | 63 | ### Via Docker image 64 | ```bash 65 | cd docker 66 | sh ./build.sh 67 | sh ./run.sh 68 | ``` 69 | 70 | ## Running experiments 71 | ### Jupyter Notebook and Google Colab 72 | [Jupyter notebook](https://github.com/locuslab/SATNet/blob/master/notebooks/Learning%20and%20Solving%20Sudoku%20via%20SATNet.ipynb) 73 | and [Google Colab](https://colab.research.google.com/drive/1dRfepPLEE8N6BBZhXz8bbLDcPnRKaOcJ#forceEdit=true&offline=true&sandboxMode=true) 74 | 75 | ### Run them manually 76 | 77 | #### Getting the datasets 78 | The [Sudoku dataset](https://powei.tw/sudoku.zip) and [Parity dataset](https://powei.tw/parity.zip) can be downloaded via 79 | 80 | ```bash 81 | wget -cq powei.tw/sudoku.zip && unzip -qq sudoku.zip 82 | wget -cq powei.tw/parity.zip && unzip -qq parity.zip 83 | ``` 84 | #### Sudoku experiments (original, permuted, and visual) 85 | ```bash 86 | python exps/sudoku.py 87 | python exps/sudoku.py --perm 88 | python exps/sudoku.py --mnist --batchSz=50 89 | ``` 90 | 91 | #### Parity experiments 92 | ```bash 93 | python exps/parity.py --seq=20 94 | python exps/parity.py --seq=40 95 | ``` 96 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pytorch/pytorch:1.13.0-cuda11.6-cudnn8-devel 2 | RUN pip install setproctitle 3 | 4 | ARG USER_ID 5 | ARG GROUP_ID 6 | ARG USER_NAME 7 | ARG HOME_DIR 8 | 9 | RUN addgroup --gid ${GROUP_ID} ${USER_NAME} || groupmod -n ${USER_NAME} $(getent group ${GROUP_ID}) 10 | RUN apt-get -q update; apt-get -q -y install sudo vim 11 | RUN conda install -y -q jupyter matplotlib 12 | RUN adduser --quiet --disabled-password --system --no-create-home --uid ${USER_ID} --gid ${GROUP_ID} --gecos '' --shell /bin/bash ${USER_NAME} 13 | RUN usermod -d ${HOME_DIR} ${USER_NAME} 14 | RUN adduser --quiet ${USER_NAME} sudo ; echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 15 | RUN apt-get install -y git 16 | 17 | RUN mkdir -p /data 18 | WORKDIR /data 19 | USER ${USER_NAME} 20 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | docker image build \ 2 | --build-arg USER_ID=$(id -u ${USER}) \ 3 | --build-arg GROUP_ID=$(id -g ${USER}) \ 4 | --build-arg USER_NAME=$(whoami) \ 5 | --build-arg HOME_DIR=$HOME \ 6 | -t satnet . 7 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | DATA_VOLUME="-v $(pwd)/..:/data" 2 | HOME_VOLUME="-v $HOME:$HOME" 3 | docker run --rm --runtime=nvidia -it --net=host --ipc=host ${DATA_VOLUME} ${HOME_VOLUME} --name=satnet satnet 4 | -------------------------------------------------------------------------------- /exps/parity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | 5 | import os 6 | import sys 7 | import csv 8 | import shutil 9 | 10 | import numpy.random as npr 11 | 12 | import torch 13 | import torch.optim as optim 14 | import torch.nn.functional as F 15 | from torch.utils.data import TensorDataset, DataLoader 16 | 17 | import satnet 18 | from tqdm.auto import tqdm 19 | 20 | class CSVLogger(object): 21 | def __init__(self, fname): 22 | self.f = open(fname, 'w') 23 | self.logger = csv.writer(self.f) 24 | 25 | def log(self, fields): 26 | self.logger.writerow(fields) 27 | self.f.flush() 28 | 29 | def main(): 30 | parser = argparse.ArgumentParser() 31 | parser.add_argument('--data_dir', type=str, default='parity') 32 | parser.add_argument('--testPct', type=float, default=0.1) 33 | parser.add_argument('--batchSz', type=int, default=100) 34 | parser.add_argument('--testBatchSz', type=int, default=500) 35 | parser.add_argument('--nEpoch', type=int, default=100) 36 | parser.add_argument('--lr', type=float, default=1e-1) 37 | parser.add_argument('--seq', type=int, default=20) 38 | parser.add_argument('--save', type=str) 39 | parser.add_argument('--m', type=int, default=4) 40 | parser.add_argument('--aux', type=int, default=4) 41 | parser.add_argument('--no_cuda', action='store_true') 42 | parser.add_argument('--adam', action='store_true') 43 | 44 | args = parser.parse_args() 45 | 46 | # For debugging: fix the random seed 47 | npr.seed(1) 48 | torch.manual_seed(7) 49 | 50 | args.cuda = not args.no_cuda and torch.cuda.is_available() 51 | if args.cuda: 52 | print('Using', torch.cuda.get_device_name(0)) 53 | torch.backends.cudnn.deterministic = True 54 | torch.backends.cudnn.benchmark = False 55 | torch.cuda.init() 56 | 57 | save = 'parity.aux{}-m{}-lr{}-bsz{}'.format( 58 | args.aux, args.m, args.lr, args.batchSz) 59 | 60 | if args.save: save = '{}-{}'.format(args.save, save) 61 | save = os.path.join('logs', save) 62 | if os.path.isdir(save): shutil.rmtree(save) 63 | os.makedirs(save) 64 | 65 | L = args.seq 66 | 67 | with open(os.path.join(args.data_dir, str(L), 'features.pt'), 'rb') as f: 68 | X = torch.load(f).float() 69 | with open(os.path.join(args.data_dir, str(L), 'labels.pt'), 'rb') as f: 70 | Y = torch.load(f).float() 71 | 72 | if args.cuda: X, Y = X.cuda(), Y.cuda() 73 | 74 | N = X.size(0) 75 | 76 | nTrain = int(N*(1-args.testPct)) 77 | nTest = N-nTrain 78 | 79 | assert(nTrain % args.batchSz == 0) 80 | assert(nTest % args.testBatchSz == 0) 81 | 82 | train_is_input = torch.IntTensor([1,1,0]).repeat(nTrain,1) 83 | test_is_input = torch.IntTensor([1,1,0]).repeat(nTest,1) 84 | if args.cuda: train_is_input, test_is_input = train_is_input.cuda(), test_is_input.cuda() 85 | 86 | train_set = TensorDataset(X[:nTrain], train_is_input, Y[:nTrain]) 87 | test_set = TensorDataset(X[nTrain:], test_is_input, Y[nTrain:]) 88 | 89 | model = satnet.SATNet(3, args.m, args.aux, prox_lam=1e-1) 90 | if args.cuda: model = model.cuda() 91 | 92 | if args.adam: 93 | optimizer = optim.Adam(model.parameters(), lr=args.lr) 94 | else: 95 | optimizer = optim.SGD(model.parameters(), lr=args.lr) 96 | 97 | train_logger = CSVLogger(os.path.join(save, 'train.csv')) 98 | test_logger = CSVLogger(os.path.join(save, 'test.csv')) 99 | fields = ['epoch', 'loss', 'err'] 100 | train_logger.log(fields) 101 | test_logger.log(fields) 102 | 103 | test(0, model, optimizer, test_logger, test_set, args.testBatchSz) 104 | for epoch in range(1, args.nEpoch+1): 105 | train(epoch, model, optimizer, train_logger, train_set, args.batchSz) 106 | test(epoch, model, optimizer, test_logger, test_set, args.testBatchSz) 107 | 108 | def apply_seq(net, zeros, batch_data, batch_is_inputs, batch_targets): 109 | y = torch.cat([batch_data[:,:2], zeros], dim=1) 110 | y = net(y, batch_is_inputs) 111 | L = batch_data.size(1) 112 | for i in range(L-2): 113 | y = torch.cat([y[:,-1].unsqueeze(1), batch_data[:,i+2].unsqueeze(1), zeros], dim=1) 114 | y = net(((y-0.5).sign()+1)/2, batch_is_inputs) 115 | loss = F.binary_cross_entropy(y[:,-1], batch_targets[:,-1]) 116 | return loss, y 117 | 118 | def run(epoch, model, optimizer, logger, dataset, batchSz, to_train): 119 | loss_final, err_final = 0, 0 120 | 121 | loader = DataLoader(dataset, batch_size=batchSz) 122 | tloader = tqdm(enumerate(loader), total=len(loader)) 123 | 124 | start = torch.zeros(batchSz, 1) 125 | if next(model.parameters()).is_cuda: start = start.cuda() 126 | 127 | for i,(data,is_input, label) in tloader: 128 | if to_train: optimizer.zero_grad() 129 | 130 | loss, pred = apply_seq(model, start, data, is_input, label) 131 | 132 | if to_train: 133 | loss.backward() 134 | optimizer.step() 135 | 136 | err = computeErr(pred, label) 137 | tloader.set_description('Epoch {} {} Loss {:.4f} Err: {:.4f}'.format( 138 | epoch, ('Train' if to_train else 'Test '), loss.item(), err)) 139 | loss_final += loss.item() 140 | err_final += err 141 | 142 | loss_final, err_final = loss_final/len(loader), err_final/len(loader) 143 | logger.log((epoch, loss_final, err_final)) 144 | 145 | if not to_train: 146 | print('TESTING SET RESULTS: Average loss: {:.4f} Err: {:.4f}'.format(loss_final, err_final)) 147 | 148 | def train(epoch, model, optimizer, logger, dataset, batchSz): 149 | run(epoch, model, optimizer, logger, dataset, batchSz, True) 150 | 151 | @torch.no_grad() 152 | def test(epoch, model, optimizer, logger, dataset, batchSz): 153 | run(epoch, model, optimizer, logger, dataset, batchSz, False) 154 | 155 | @torch.no_grad() 156 | def computeErr(pred, target): 157 | y = (pred[:,-1]-0.5) 158 | t = (target[:,-1]-0.5) 159 | correct = ((y * t).sign()+1.)/2 160 | acc = correct.sum().float()/target.size(0) 161 | 162 | return 1-float(acc) 163 | 164 | if __name__ == '__main__': 165 | main() 166 | -------------------------------------------------------------------------------- /exps/sudoku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Partly derived from: 4 | # https://github.com/locuslab/optnet/blob/master/sudoku/train.py 5 | 6 | import argparse 7 | 8 | import os 9 | import shutil 10 | import csv 11 | 12 | import numpy as np 13 | import numpy.random as npr 14 | #import setproctitle 15 | 16 | import torch 17 | import torch.nn as nn 18 | import torch.optim as optim 19 | import torch.nn.functional as F 20 | from torch.utils.data import TensorDataset, DataLoader 21 | from tqdm.auto import tqdm 22 | 23 | import satnet 24 | 25 | class SudokuSolver(nn.Module): 26 | def __init__(self, boardSz, aux, m): 27 | super(SudokuSolver, self).__init__() 28 | n = boardSz**6 29 | self.sat = satnet.SATNet(n, m, aux) 30 | 31 | def forward(self, y_in, mask): 32 | out = self.sat(y_in, mask) 33 | return out 34 | 35 | class DigitConv(nn.Module): 36 | ''' 37 | Convolutional neural network for MNIST digit recognition. From: 38 | https://github.com/pytorch/examples/blob/master/mnist/main.py 39 | ''' 40 | def __init__(self): 41 | super(DigitConv, self).__init__() 42 | self.conv1 = nn.Conv2d(1, 20, 5, 1) 43 | self.conv2 = nn.Conv2d(20, 50, 5, 1) 44 | self.fc1 = nn.Linear(4*4*50, 500) 45 | self.fc2 = nn.Linear(500, 10) 46 | 47 | def forward(self, x): 48 | x = F.relu(self.conv1(x)) 49 | x = F.max_pool2d(x, 2, 2) 50 | x = F.relu(self.conv2(x)) 51 | x = F.max_pool2d(x, 2, 2) 52 | x = x.view(-1, 4*4*50) 53 | x = F.relu(self.fc1(x)) 54 | x = self.fc2(x) 55 | return F.softmax(x, dim=1)[:,:9].contiguous() 56 | 57 | class MNISTSudokuSolver(nn.Module): 58 | def __init__(self, boardSz, aux, m): 59 | super(MNISTSudokuSolver, self).__init__() 60 | self.digit_convnet = DigitConv() 61 | self.sudoku_solver = SudokuSolver(boardSz, aux, m) 62 | self.boardSz = boardSz 63 | self.nSq = boardSz**2 64 | 65 | def forward(self, x, is_inputs): 66 | nBatch = x.shape[0] 67 | x = x.flatten(start_dim = 0, end_dim = 1) 68 | digit_guess = self.digit_convnet(x) 69 | puzzles = digit_guess.view(nBatch, self.nSq * self.nSq * self.nSq) 70 | 71 | solution = self.sudoku_solver(puzzles, is_inputs) 72 | return solution 73 | 74 | class CSVLogger(object): 75 | def __init__(self, fname): 76 | self.f = open(fname, 'w') 77 | self.logger = csv.writer(self.f) 78 | 79 | def log(self, fields): 80 | self.logger.writerow(fields) 81 | self.f.flush() 82 | 83 | class FigLogger(object): 84 | def __init__(self, fig, base_ax, title): 85 | self.colors = ['tab:red', 'tab:blue'] 86 | self.labels = ['Loss (entropy)', 'Error'] 87 | self.markers = ['d', '.'] 88 | self.axes = [base_ax, base_ax.twinx()] 89 | base_ax.set_xlabel('Epochs') 90 | base_ax.set_title(title) 91 | 92 | for i, ax in enumerate(self.axes): 93 | ax.set_ylabel(self.labels[i], color=self.colors[i]) 94 | ax.tick_params(axis='y', labelcolor=self.colors[i]) 95 | 96 | self.reset() 97 | self.fig = fig 98 | 99 | def log(self, args): 100 | for i, arg in enumerate(args[-2:]): 101 | self.curves[i].append(arg) 102 | x = list(range(len(self.curves[i]))) 103 | self.axes[i].plot(x, self.curves[i], self.colors[i], marker=self.markers[i]) 104 | self.axes[i].set_ylim(0, 1.05) 105 | 106 | self.fig.canvas.draw() 107 | 108 | def reset(self): 109 | for ax in self.axes: 110 | for line in ax.lines: 111 | line.remove() 112 | self.curves = [[], []] 113 | 114 | def print_header(msg): 115 | print('===>', msg) 116 | 117 | def find_unperm(perm): 118 | unperm = torch.zeros_like(perm) 119 | for i in range(perm.size(0)): 120 | unperm[perm[i]] = i 121 | return unperm 122 | 123 | def main(): 124 | parser = argparse.ArgumentParser() 125 | parser.add_argument('--data_dir', type=str, default='sudoku') 126 | parser.add_argument('--boardSz', type=int, default=3) 127 | parser.add_argument('--batchSz', type=int, default=40) 128 | parser.add_argument('--testBatchSz', type=int, default=40) 129 | parser.add_argument('--aux', type=int, default=300) 130 | parser.add_argument('--m', type=int, default=600) 131 | parser.add_argument('--nEpoch', type=int, default=100) 132 | parser.add_argument('--testPct', type=float, default=0.1) 133 | parser.add_argument('--lr', type=float, default=2e-3) 134 | parser.add_argument('--save', type=str) 135 | parser.add_argument('--model', type=str) 136 | parser.add_argument('--no_cuda', action='store_true') 137 | parser.add_argument('--mnist', action='store_true') 138 | parser.add_argument('--perm', action='store_true') 139 | 140 | args = parser.parse_args() 141 | 142 | # For debugging: fix the random seed 143 | npr.seed(1) 144 | torch.manual_seed(7) 145 | 146 | args.cuda = not args.no_cuda and torch.cuda.is_available() 147 | if args.cuda: 148 | print('Using', torch.cuda.get_device_name(0)) 149 | torch.backends.cudnn.deterministic = True 150 | torch.backends.cudnn.benchmark = False 151 | torch.cuda.init() 152 | 153 | save = 'sudoku{}{}.boardSz{}-aux{}-m{}-lr{}-bsz{}'.format( 154 | '.perm' if args.perm else '', '.mnist' if args.mnist else '', 155 | args.boardSz, args.aux, args.m, args.lr, args.batchSz) 156 | if args.save: save = '{}-{}'.format(args.save, save) 157 | save = os.path.join('logs', save) 158 | if os.path.isdir(save): shutil.rmtree(save) 159 | os.makedirs(save) 160 | 161 | #setproctitle.setproctitle('sudoku.{}'.format(save)) 162 | 163 | print_header('Loading data') 164 | 165 | with open(os.path.join(args.data_dir, 'features.pt'), 'rb') as f: 166 | X_in = torch.load(f) 167 | with open(os.path.join(args.data_dir, 'features_img.pt'), 'rb') as f: 168 | Ximg_in = torch.load(f) 169 | with open(os.path.join(args.data_dir, 'labels.pt'), 'rb') as f: 170 | Y_in = torch.load(f) 171 | with open(os.path.join(args.data_dir, 'perm.pt'), 'rb') as f: 172 | perm = torch.load(f) 173 | 174 | N = X_in.size(0) 175 | nTrain = int(N*(1.-args.testPct)) 176 | nTest = N-nTrain 177 | assert(nTrain % args.batchSz == 0) 178 | assert(nTest % args.testBatchSz == 0) 179 | 180 | print_header('Forming inputs') 181 | X, Ximg, Y, is_input = process_inputs(X_in, Ximg_in, Y_in, args.boardSz) 182 | data = Ximg if args.mnist else X 183 | if args.cuda: data, is_input, Y = data.cuda(), is_input.cuda(), Y.cuda() 184 | 185 | unperm = None 186 | if args.perm and not args.mnist: 187 | print('Applying permutation') 188 | data[:,:], Y[:,:], is_input[:,:] = data[:,perm], Y[:,perm], is_input[:,perm] 189 | unperm = find_unperm(perm) 190 | 191 | train_set = TensorDataset(data[:nTrain], is_input[:nTrain], Y[:nTrain]) 192 | test_set = TensorDataset(data[nTrain:], is_input[nTrain:], Y[nTrain:]) 193 | 194 | print_header('Building model') 195 | if args.mnist: 196 | model = MNISTSudokuSolver(args.boardSz, args.aux, args.m) 197 | else: 198 | model = SudokuSolver(args.boardSz, args.aux, args.m) 199 | 200 | if args.cuda: model = model.cuda() 201 | 202 | if args.mnist: 203 | optimizer = optim.Adam([ 204 | {'params': model.sudoku_solver.parameters(), 'lr': args.lr}, 205 | {'params': model.digit_convnet.parameters(), 'lr': 1e-5}, 206 | ]) 207 | else: 208 | optimizer = optim.Adam(model.parameters(), lr=args.lr) 209 | 210 | if args.model: 211 | model.load_state_dict(torch.load(args.model)) 212 | 213 | train_logger = CSVLogger(os.path.join(save, 'train.csv')) 214 | test_logger = CSVLogger(os.path.join(save, 'test.csv')) 215 | fields = ['epoch', 'loss', 'err'] 216 | train_logger.log(fields) 217 | test_logger.log(fields) 218 | 219 | test(args.boardSz, 0, model, optimizer, test_logger, test_set, args.testBatchSz, unperm) 220 | for epoch in range(1, args.nEpoch+1): 221 | train(args.boardSz, epoch, model, optimizer, train_logger, train_set, args.batchSz, unperm) 222 | test(args.boardSz, epoch, model, optimizer, test_logger, test_set, args.testBatchSz, unperm) 223 | #torch.save(model.state_dict(), os.path.join(save, 'it'+str(epoch)+'.pth')) 224 | 225 | def process_inputs(X, Ximg, Y, boardSz): 226 | is_input = X.sum(dim=3, keepdim=True).expand_as(X).int().sign() 227 | 228 | Ximg = Ximg.flatten(start_dim=1, end_dim=2) 229 | Ximg = Ximg.unsqueeze(2).float() 230 | 231 | X = X.view(X.size(0), -1) 232 | Y = Y.view(Y.size(0), -1) 233 | is_input = is_input.view(is_input.size(0), -1) 234 | 235 | return X, Ximg, Y, is_input 236 | 237 | def run(boardSz, epoch, model, optimizer, logger, dataset, batchSz, to_train=False, unperm=None): 238 | 239 | loss_final, err_final = 0, 0 240 | 241 | loader = DataLoader(dataset, batch_size=batchSz) 242 | tloader = tqdm(enumerate(loader), total=len(loader)) 243 | 244 | for i,(data,is_input,label) in tloader: 245 | if to_train: optimizer.zero_grad() 246 | preds = model(data.contiguous(), is_input.contiguous()) 247 | loss = nn.functional.binary_cross_entropy(preds, label) 248 | 249 | if to_train: 250 | loss.backward() 251 | optimizer.step() 252 | 253 | err = computeErr(preds.data, boardSz, unperm)/batchSz 254 | tloader.set_description('Epoch {} {} Loss {:.4f} Err: {:.4f}'.format(epoch, ('Train' if to_train else 'Test '), loss.item(), err)) 255 | loss_final += loss.item() 256 | err_final += err 257 | 258 | loss_final, err_final = loss_final/len(loader), err_final/len(loader) 259 | logger.log((epoch, loss_final, err_final)) 260 | 261 | if not to_train: 262 | print('TESTING SET RESULTS: Average loss: {:.4f} Err: {:.4f}'.format(loss_final, err_final)) 263 | 264 | #print('memory: {:.2f} MB, cached: {:.2f} MB'.format(torch.cuda.memory_allocated()/2.**20, torch.cuda.memory_cached()/2.**20)) 265 | torch.cuda.empty_cache() 266 | 267 | def train(args, epoch, model, optimizer, logger, dataset, batchSz, unperm=None): 268 | run(args, epoch, model, optimizer, logger, dataset, batchSz, True, unperm) 269 | 270 | @torch.no_grad() 271 | def test(args, epoch, model, optimizer, logger, dataset, batchSz, unperm=None): 272 | run(args, epoch, model, optimizer, logger, dataset, batchSz, False, unperm) 273 | 274 | @torch.no_grad() 275 | def computeErr(pred_flat, n, unperm): 276 | if unperm is not None: pred_flat[:,:] = pred_flat[:,unperm] 277 | 278 | nsq = n ** 2 279 | pred = pred_flat.view(-1, nsq, nsq, nsq) 280 | 281 | batchSz = pred.size(0) 282 | s = (nsq-1)*nsq//2 # 0 + 1 + ... + n^2-1 283 | I = torch.max(pred, 3)[1].squeeze().view(batchSz, nsq, nsq) 284 | 285 | def invalidGroups(x): 286 | valid = (x.min(1)[0] == 0) 287 | valid *= (x.max(1)[0] == nsq-1) 288 | valid *= (x.sum(1) == s) 289 | return valid.bitwise_not() 290 | 291 | boardCorrect = torch.ones(batchSz).type_as(pred) 292 | for j in range(nsq): 293 | # Check the jth row and column. 294 | boardCorrect[invalidGroups(I[:,j,:])] = 0 295 | boardCorrect[invalidGroups(I[:,:,j])] = 0 296 | 297 | # Check the jth block. 298 | row, col = n*(j // n), n*(j % n) 299 | M = invalidGroups(I[:,row:row+n,col:col+n].contiguous().view(batchSz,-1)) 300 | boardCorrect[M] = 0 301 | 302 | if boardCorrect.sum() == 0: 303 | return batchSz 304 | 305 | return float(batchSz-boardCorrect.sum()) 306 | 307 | if __name__=='__main__': 308 | main() 309 | -------------------------------------------------------------------------------- /images/forward_pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locuslab/SATNet/50897688eae47bf765c1d9ed9a7c6f5419d62a9a/images/forward_pass.png -------------------------------------------------------------------------------- /images/mnist_sudoku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locuslab/SATNet/50897688eae47bf765c1d9ed9a7c6f5419d62a9a/images/mnist_sudoku.png -------------------------------------------------------------------------------- /images/poster_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locuslab/SATNet/50897688eae47bf765c1d9ed9a7c6f5419d62a9a/images/poster_forward.png -------------------------------------------------------------------------------- /notebooks/Learning and Solving Sudoku via SATNet.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "import shutil\n", 11 | "import argparse\n", 12 | "from collections import namedtuple\n", 13 | "\n", 14 | "import numpy as np\n", 15 | "import numpy.random as npr\n", 16 | "\n", 17 | "import torch\n", 18 | "import torch.nn as nn\n", 19 | "import torch.optim as optim\n", 20 | "import torch.nn.functional as F\n", 21 | "from torch.utils.data import TensorDataset, DataLoader\n", 22 | "\n", 23 | "%matplotlib inline\n", 24 | "import matplotlib.pyplot as plt\n", 25 | "from IPython.display import display, Markdown, Latex, clear_output" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "# Introduction to SATNet" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "SATNet is a differentiable (smoothed) maximum satisfiability (MAXSAT) solver that can be integrated into the loop of larger deep learning systems. Our (approximate) solver is based upon a fast coordinate descent approach to solving the semidefinite program (SDP) associated with the MAXSAT problem.\n", 40 | "\n", 41 | "The code below reproduces the Sudoku experiments from our paper \"SATNet: Bridging deep learning and logical reasoning using a differentiable satisfiability solver.\" These experiments show that by integrating the SATNet solver into end-to-end learning systems, we can learn the logical structure of challenging problems in a minimally supervised fashion. In particular, this notebook shows how we can learn to:\n", 42 | "* Play **9×9 Sudoku (original and permuted)** solely from examples.\n", 43 | "* Solve a **\"visual Sudoku\"** problem that maps images of Sudoku puzzles to their associated logical solutions. \n", 44 | "\n", 45 | "For more details and discussion about these experiments, please see the [SATNet paper](https://icml.cc/Conferences/2019/Schedule?showEvent=3947)." 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 2, 51 | "metadata": {}, 52 | "outputs": [ 53 | { 54 | "name": "stdout", 55 | "output_type": "stream", 56 | "text": [ 57 | "SATNet document\n", 58 | " Apply a SATNet layer to complete the input probabilities.\n", 59 | "\n", 60 | " Args:\n", 61 | " n: Number of input variables.\n", 62 | " m: Rank of the clause matrix.\n", 63 | " aux: Number of auxiliary variables.\n", 64 | "\n", 65 | " max_iter: Maximum number of iterations for solving\n", 66 | " the inner optimization problem.\n", 67 | " Default: 40\n", 68 | " eps: The stopping threshold for the inner optimizaiton problem.\n", 69 | " The inner Mixing method will stop when the function decrease\n", 70 | " is less then eps times the initial function decrease.\n", 71 | " Default: 1e-4\n", 72 | " prox_lam: The diagonal increment in the backward linear system\n", 73 | " to make the backward pass more stable.\n", 74 | " Default: 1e-2\n", 75 | " weight_normalize: Set true to perform normlization for init weights.\n", 76 | " Default: True\n", 77 | "\n", 78 | " Inputs: (z, is_input)\n", 79 | " **z** of shape `(batch, n)`: \n", 80 | " Float tensor containing the probabilities (must be in [0,1]).\n", 81 | " **is_input** of shape `(batch, n)`: \n", 82 | " Int tensor indicating which **z** is a input.\n", 83 | "\n", 84 | " Outputs: z\n", 85 | " **z** of shape `(batch, n)`:\n", 86 | " The prediction probabiolities.\n", 87 | "\n", 88 | " Attributes: S\n", 89 | " **S** of shape `(n, m)`:\n", 90 | " The learnable clauses matrix containing `m` clauses \n", 91 | " for the `n` variables.\n", 92 | "\n", 93 | " Examples:\n", 94 | " >>> sat = satnet.SATNet(3, 4, aux=5)\n", 95 | " >>> z = torch.randn(2, 3)\n", 96 | " >>> is_input = torch.IntTensor([[1, 1, 0], [1,0,1]])\n", 97 | " >>> pred = sat(z, is_input)\n", 98 | " \n" 99 | ] 100 | } 101 | ], 102 | "source": [ 103 | "import satnet\n", 104 | "print('SATNet document\\n', satnet.SATNet.__doc__)" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "# Building SATNet-based Models" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "To solve **Sudoku** and a **permuted version of Sudoku**: We construct a SATNet-based SudokuSolver layer that takes as input a logical (bit) representation of the initial Sudoku board along with a mask representing which bits must be learned (i.e. all bits in empty Sudoku cells). This input is vectorized. Given this input, the SudokuSolver layer then outputs a bit representation of the Sudoku board with guesses for the unknown bits." 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 3, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "class SudokuSolver(nn.Module):\n", 128 | " def __init__(self, boardSz, aux, m):\n", 129 | " super(SudokuSolver, self).__init__()\n", 130 | " n = boardSz**6\n", 131 | " self.sat = satnet.SATNet(n, m, aux)\n", 132 | "\n", 133 | " def forward(self, y_in, mask):\n", 134 | " out = self.sat(y_in, mask)\n", 135 | " del y_in, mask\n", 136 | " return out" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "To solve **\"visual Sudoku\"**: We construct a (standard) convolutional neural network for MNIST digit recognition and train it end-to-end with our SudokuSolver layer. This architecture takes in an image representation of a Sudoku board constructed with MNIST digits. Each MNIST digit is classified by the convolutional network, and the resulting (estimated) logical representation of the initial Sudoku board is then fed as input to the SudokuSolver layer. (As described earlier, the SudokuSolver layer then outputs a bit representation of the Sudoku board with guesses for the unknown bits.)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": 4, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "class DigitConv(nn.Module):\n", 153 | " '''\n", 154 | " Convolutional neural network for MNIST digit recognition. From:\n", 155 | " https://github.com/pytorch/examples/blob/master/mnist/main.py\n", 156 | " '''\n", 157 | " def __init__(self):\n", 158 | " super(DigitConv, self).__init__()\n", 159 | " self.conv1 = nn.Conv2d(1, 20, 5, 1)\n", 160 | " self.conv2 = nn.Conv2d(20, 50, 5, 1)\n", 161 | " self.fc1 = nn.Linear(4*4*50, 500)\n", 162 | " self.fc2 = nn.Linear(500, 10)\n", 163 | "\n", 164 | " def forward(self, x):\n", 165 | " x = F.relu(self.conv1(x))\n", 166 | " x = F.max_pool2d(x, 2, 2)\n", 167 | " x = F.relu(self.conv2(x))\n", 168 | " x = F.max_pool2d(x, 2, 2)\n", 169 | " x = x.view(-1, 4*4*50)\n", 170 | " x = F.relu(self.fc1(x))\n", 171 | " x = self.fc2(x)\n", 172 | " return F.softmax(x, dim=1)[:,:9].contiguous()\n", 173 | "\n", 174 | "class MNISTSudokuSolver(nn.Module):\n", 175 | " def __init__(self, boardSz, aux, m):\n", 176 | " super(MNISTSudokuSolver, self).__init__()\n", 177 | " self.digit_convnet = DigitConv()\n", 178 | " self.sudoku_solver = SudokuSolver(boardSz, aux, m)\n", 179 | " self.boardSz = boardSz\n", 180 | " self.nSq = boardSz**2\n", 181 | " \n", 182 | " def forward(self, x, is_inputs):\n", 183 | " nBatch = x.shape[0]\n", 184 | " x = x.flatten(start_dim = 0, end_dim = 1)\n", 185 | " digit_guess = self.digit_convnet(x)\n", 186 | " puzzles = digit_guess.view(nBatch, self.nSq * self.nSq * self.nSq)\n", 187 | "\n", 188 | " solution = self.sudoku_solver(puzzles, is_inputs)\n", 189 | " return solution" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "The experimental parameters we use in the paper are below." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 5, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "from exps.sudoku import train, test, FigLogger, find_unperm\n", 206 | "args_dict = {'lr': 2e-3, \n", 207 | " 'cuda': torch.cuda.is_available(), \n", 208 | " 'batchSz': 40,\n", 209 | " 'mnistBatchSz': 50,\n", 210 | " 'boardSz': 3, # for 9x9 Sudoku\n", 211 | " 'm': 600,\n", 212 | " 'aux': 300,\n", 213 | " 'nEpoch': 100\n", 214 | " }\n", 215 | "args = namedtuple('Args', args_dict.keys())(*args_dict.values())" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "# The Sudoku Datasets" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": {}, 228 | "source": [ 229 | "We use and/or create the following datasets:\n", 230 | "* **Sudoku:** We generate 10K 9x9 Sudoku boards (9K test/1K train) using code available [here](https://github.com/Kyubyong/sudoku) and represent them via bit (one-hot) representations.\n", 231 | "* **Permuted Sudoku:** We apply a fixed permutation to the 10K Sudoku board bit representations generated for the Sudoku experiment.\n", 232 | "* **Visual Sudoku:** We construct versions of the 10K Sudoku boards generated for the Sudoku experiment in which each board cell is represented by a (randomly-selected) MNIST digit. (MNIST digits are also split into train/test sets, with train and test MNIST digits applied only to train and test Sudoku boards, respectively.)\n", 233 | "\n", 234 | "The code below reads and processes these datasets for use with the architectures constructed above. A sample Sudoku board, its associated bit representation, and its associated MNIST representation are displayed below." 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": 6, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "def process_inputs(X, Ximg, Y, boardSz):\n", 244 | " is_input = X.sum(dim=3, keepdim=True).expand_as(X).int().sign()\n", 245 | "\n", 246 | " Ximg = Ximg.flatten(start_dim=1, end_dim=2)\n", 247 | " Ximg = Ximg.unsqueeze(2).float()\n", 248 | "\n", 249 | " X = X.view(X.size(0), -1)\n", 250 | " Y = Y.view(Y.size(0), -1)\n", 251 | " is_input = is_input.view(is_input.size(0), -1)\n", 252 | "\n", 253 | " return X, Ximg, Y, is_input\n", 254 | "\n", 255 | "with open('sudoku/features.pt', 'rb') as f:\n", 256 | " X_in = torch.load(f)\n", 257 | "with open('sudoku/features_img.pt', 'rb') as f:\n", 258 | " Ximg_in = torch.load(f)\n", 259 | "with open('sudoku/labels.pt', 'rb') as f:\n", 260 | " Y_in = torch.load(f)\n", 261 | "with open('sudoku/perm.pt', 'rb') as f:\n", 262 | " perm = torch.load(f)\n", 263 | "\n", 264 | "X, Ximg, Y, is_input = process_inputs(X_in, Ximg_in, Y_in, args.boardSz)\n", 265 | "if args.cuda: X, Ximg, is_input, Y = X.cuda(), Ximg.cuda(), is_input.cuda(), Y.cuda()\n", 266 | "\n", 267 | "N = X_in.size(0)\n", 268 | "nTrain = int(N*0.9)\n", 269 | "\n", 270 | "sudoku_train = TensorDataset(X[:nTrain], is_input[:nTrain], Y[:nTrain])\n", 271 | "sudoku_test = TensorDataset(X[nTrain:], is_input[nTrain:], Y[nTrain:])\n", 272 | "perm_train = TensorDataset(X[:nTrain,perm], is_input[:nTrain,perm], Y[:nTrain,perm])\n", 273 | "perm_test = TensorDataset(X[nTrain:,perm], is_input[nTrain:,perm], Y[nTrain:,perm])\n", 274 | "mnist_train = TensorDataset(Ximg[:nTrain], is_input[:nTrain], Y[:nTrain])\n", 275 | "mnist_test = TensorDataset(Ximg[nTrain:], is_input[nTrain:], Y[nTrain:])" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": 7, 281 | "metadata": {}, 282 | "outputs": [ 283 | { 284 | "data": { 285 | "text/markdown": [ 286 | "## Sudoku" 287 | ], 288 | "text/plain": [ 289 | "" 290 | ] 291 | }, 292 | "metadata": {}, 293 | "output_type": "display_data" 294 | }, 295 | { 296 | "name": "stdout", 297 | "output_type": "stream", 298 | "text": [ 299 | "tensor([[6, 7, 0, 0, 0, 0, 0, 0, 5],\n", 300 | " [0, 0, 3, 0, 4, 0, 0, 8, 2],\n", 301 | " [0, 4, 0, 0, 0, 5, 1, 3, 6],\n", 302 | " [0, 0, 0, 7, 3, 0, 0, 9, 0],\n", 303 | " [3, 0, 4, 2, 0, 6, 0, 7, 0],\n", 304 | " [0, 0, 1, 0, 9, 0, 6, 0, 0],\n", 305 | " [5, 0, 9, 0, 0, 8, 0, 0, 0],\n", 306 | " [0, 0, 0, 9, 5, 0, 2, 0, 8],\n", 307 | " [0, 0, 0, 1, 2, 7, 4, 0, 0]])\n", 308 | "\n" 309 | ] 310 | }, 311 | { 312 | "data": { 313 | "text/markdown": [ 314 | "## One-hot encoded Boolean Sudoku" 315 | ], 316 | "text/plain": [ 317 | "" 318 | ] 319 | }, 320 | "metadata": {}, 321 | "output_type": "display_data" 322 | }, 323 | { 324 | "name": "stdout", 325 | "output_type": "stream", 326 | "text": [ 327 | "tensor([0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,\n", 328 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 329 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 330 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 331 | " 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 332 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,\n", 333 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,\n", 334 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 335 | " 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,\n", 336 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,\n", 337 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 338 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.,\n", 339 | " 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,\n", 340 | " 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 341 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 342 | " 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,\n", 343 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 344 | " 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 345 | " 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 346 | " 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,\n", 347 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,\n", 348 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,\n", 349 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 350 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 351 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.,\n", 352 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,\n", 353 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 354 | " 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 355 | " 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 356 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.,\n", 357 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 358 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 359 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 360 | " 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0.,\n", 361 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,\n", 362 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.,\n", 363 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 364 | " 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 365 | " 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,\n", 366 | " 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", 367 | " 0., 0., 0., 0., 0., 0., 0., 0., 0.], device='cuda:0')\n" 368 | ] 369 | }, 370 | { 371 | "data": { 372 | "text/markdown": [ 373 | "## MNIST Sudoku" 374 | ], 375 | "text/plain": [ 376 | "" 377 | ] 378 | }, 379 | "metadata": {}, 380 | "output_type": "display_data" 381 | }, 382 | { 383 | "data": { 384 | "image/png": "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\n", 385 | "text/plain": [ 386 | "
" 387 | ] 388 | }, 389 | "metadata": { 390 | "needs_background": "light" 391 | }, 392 | "output_type": "display_data" 393 | } 394 | ], 395 | "source": [ 396 | "def show_sudoku(raw):\n", 397 | " return (torch.argmax(raw,2)+1)*(raw.sum(2).long())\n", 398 | "\n", 399 | "def show_mnist_sudoku(raw):\n", 400 | " A = raw.numpy()\n", 401 | " digits = np.concatenate(np.concatenate(A,axis=1), axis=1).astype(np.uint8)\n", 402 | " linewidth = 2\n", 403 | " board = np.zeros((digits.shape[0]+linewidth*4, digits.shape[1]+linewidth*4), dtype=np.uint8)\n", 404 | " gridwidth = digits.shape[0]//3\n", 405 | "\n", 406 | " board[:] = 255\n", 407 | " for i in range(3):\n", 408 | " for j in range(3):\n", 409 | " xoff = linewidth+(linewidth+gridwidth)*i\n", 410 | " yoff = linewidth+(linewidth+gridwidth)*j\n", 411 | " xst = gridwidth*i\n", 412 | " yst = gridwidth*j\n", 413 | " board[xoff:xoff+gridwidth, yoff:yoff+gridwidth] = digits[xst:xst+gridwidth, yst:yst+gridwidth]\n", 414 | "\n", 415 | " #img = Image.fromarray(255-board)\n", 416 | " plt.imshow(255-board, cmap='gray')\n", 417 | "\n", 418 | "display(Markdown('## Sudoku'))\n", 419 | "print(show_sudoku(X_in[0]))\n", 420 | "print()\n", 421 | "display(Markdown('## One-hot encoded Boolean Sudoku'))\n", 422 | "print(X[0])\n", 423 | " \n", 424 | "display(Markdown('## MNIST Sudoku'))\n", 425 | "show_mnist_sudoku(Ximg_in[0])" 426 | ] 427 | }, 428 | { 429 | "cell_type": "markdown", 430 | "metadata": {}, 431 | "source": [ 432 | "# The 9x9 Sudoku Experiment" 433 | ] 434 | }, 435 | { 436 | "cell_type": "markdown", 437 | "metadata": {}, 438 | "source": [ 439 | "The results for our 9x9 Sudoku experiment are below. In this experiment, we:\n", 440 | "* **Input** a logical (bit) representation of the initial (unsolved) Sudoku board along with a mask representing which bits must be learned (i.e. all bits in empty Sudoku cells). This input is vectorized, which means that our SATNet model cannot exploit the locality structure of the input Sudoku grid when learning to solve puzzles.\n", 441 | "* **Output** a bit representation of the Sudoku board with guesses for the unknown bits." 442 | ] 443 | }, 444 | { 445 | "cell_type": "code", 446 | "execution_count": 8, 447 | "metadata": {}, 448 | "outputs": [], 449 | "source": [ 450 | "%%capture\n", 451 | "sudoku_model = SudokuSolver(args.boardSz, args.aux, args.m)\n", 452 | "if args.cuda: sudoku_model = sudoku_model.cuda()\n", 453 | " \n", 454 | "optimizer = optim.Adam(sudoku_model.parameters(), lr=args.lr)\n", 455 | "\n", 456 | "fig, axes = plt.subplots(1,2, figsize=(10,4))\n", 457 | "plt.subplots_adjust(wspace=0.4)\n", 458 | "train_logger = FigLogger(fig, axes[0], 'Traininig')\n", 459 | "test_logger = FigLogger(fig, axes[1], 'Testing')" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 9, 465 | "metadata": {}, 466 | "outputs": [ 467 | { 468 | "data": { 469 | "image/png": "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\n", 470 | "text/plain": [ 471 | "
" 472 | ] 473 | }, 474 | "metadata": {}, 475 | "output_type": "display_data" 476 | } 477 | ], 478 | "source": [ 479 | "test(args.boardSz, 0, sudoku_model, optimizer, test_logger, sudoku_test, args.batchSz)\n", 480 | "for epoch in range(1, args.nEpoch+1):\n", 481 | " train(args.boardSz, epoch, sudoku_model, optimizer, train_logger, sudoku_train, args.batchSz)\n", 482 | " test(args.boardSz, epoch, sudoku_model, optimizer, test_logger, sudoku_test, args.batchSz)\n", 483 | " clear_output()\n", 484 | " display(fig)" 485 | ] 486 | }, 487 | { 488 | "cell_type": "markdown", 489 | "metadata": {}, 490 | "source": [ 491 | "# The Permuted 9x9 Sudoku Experiment" 492 | ] 493 | }, 494 | { 495 | "cell_type": "markdown", 496 | "metadata": {}, 497 | "source": [ 498 | "The results for our permuted 9x9 Sudoku experiment are below. In this experiment, we:\n", 499 | "* **Input** the same inputs as in the original 9x9 Sudoku experiment, but with a fixed permutation applied.\n", 500 | "* **Output** a bit representation of the permuted Sudoku board with guesses for the unknown bits." 501 | ] 502 | }, 503 | { 504 | "cell_type": "code", 505 | "execution_count": 10, 506 | "metadata": {}, 507 | "outputs": [], 508 | "source": [ 509 | "%%capture\n", 510 | "perm_model = SudokuSolver(args.boardSz, args.aux, args.m)\n", 511 | "if args.cuda: perm_model = perm_model.cuda()\n", 512 | " \n", 513 | "optimizer = optim.Adam(perm_model.parameters(), lr=args.lr)\n", 514 | "\n", 515 | "fig, axes = plt.subplots(1,2, figsize=(10,4))\n", 516 | "plt.subplots_adjust(wspace=0.4)\n", 517 | "train_logger = FigLogger(fig, axes[0], 'Traininig')\n", 518 | "test_logger = FigLogger(fig, axes[1], 'Testing')" 519 | ] 520 | }, 521 | { 522 | "cell_type": "code", 523 | "execution_count": 11, 524 | "metadata": {}, 525 | "outputs": [ 526 | { 527 | "data": { 528 | "image/png": "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\n", 529 | "text/plain": [ 530 | "
" 531 | ] 532 | }, 533 | "metadata": {}, 534 | "output_type": "display_data" 535 | } 536 | ], 537 | "source": [ 538 | "unperm = find_unperm(perm)\n", 539 | "test(args.boardSz, 0, perm_model, optimizer, test_logger, perm_test, args.batchSz, unperm)\n", 540 | "for epoch in range(1, args.nEpoch+1):\n", 541 | " train(args.boardSz, epoch, perm_model, optimizer, train_logger, perm_train, args.batchSz, unperm)\n", 542 | " test(args.boardSz, epoch, perm_model, optimizer, test_logger, perm_test, args.batchSz, unperm)\n", 543 | " clear_output()\n", 544 | " display(fig)" 545 | ] 546 | }, 547 | { 548 | "cell_type": "markdown", 549 | "metadata": {}, 550 | "source": [ 551 | "# The End-to-End MNIST Sudoku (\"Visual Sudoku\") Experiment" 552 | ] 553 | }, 554 | { 555 | "cell_type": "markdown", 556 | "metadata": {}, 557 | "source": [ 558 | "The results for our permuted 9x9 Sudoku experiment are below. In this experiment, we:\n", 559 | "* **Input** an image representation of the initial (unsolved) Sudoku board.\n", 560 | "* **Output** a bit representation of the Sudoku board with guesses for the unknown bits." 561 | ] 562 | }, 563 | { 564 | "cell_type": "code", 565 | "execution_count": 12, 566 | "metadata": {}, 567 | "outputs": [], 568 | "source": [ 569 | "%%capture\n", 570 | "mnist_sudoku = MNISTSudokuSolver(args.boardSz, args.aux, args.m)\n", 571 | "if args.cuda: mnist_sudoku = mnist_sudoku.cuda()\n", 572 | " \n", 573 | "optimizer = optim.Adam([\n", 574 | " {'params': mnist_sudoku.sudoku_solver.parameters(), 'lr': args.lr},\n", 575 | " {'params': mnist_sudoku.digit_convnet.parameters(), 'lr': 1e-5},\n", 576 | " ])\n", 577 | "\n", 578 | "fig, axes = plt.subplots(1,2, figsize=(10,4))\n", 579 | "plt.subplots_adjust(wspace=0.4)\n", 580 | "train_logger = FigLogger(fig, axes[0], 'Traininig')\n", 581 | "test_logger = FigLogger(fig, axes[1], 'Testing')" 582 | ] 583 | }, 584 | { 585 | "cell_type": "code", 586 | "execution_count": 13, 587 | "metadata": {}, 588 | "outputs": [ 589 | { 590 | "data": { 591 | "image/png": "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\n", 592 | "text/plain": [ 593 | "
" 594 | ] 595 | }, 596 | "metadata": {}, 597 | "output_type": "display_data" 598 | } 599 | ], 600 | "source": [ 601 | "test(args.boardSz, 0, mnist_sudoku, optimizer, test_logger, mnist_test, args.mnistBatchSz)\n", 602 | "for epoch in range(1, args.nEpoch+1):\n", 603 | " train(args.boardSz, epoch, mnist_sudoku, optimizer, train_logger, mnist_train, args.mnistBatchSz)\n", 604 | " test(args.boardSz, epoch, mnist_sudoku, optimizer, test_logger, mnist_test, args.mnistBatchSz)\n", 605 | " clear_output()\n", 606 | " display(fig)" 607 | ] 608 | }, 609 | { 610 | "cell_type": "code", 611 | "execution_count": null, 612 | "metadata": {}, 613 | "outputs": [], 614 | "source": [] 615 | } 616 | ], 617 | "metadata": { 618 | "kernelspec": { 619 | "display_name": "Python 3", 620 | "language": "python", 621 | "name": "python3" 622 | }, 623 | "language_info": { 624 | "codemirror_mode": { 625 | "name": "ipython", 626 | "version": 3 627 | }, 628 | "file_extension": ".py", 629 | "mimetype": "text/x-python", 630 | "name": "python", 631 | "nbconvert_exporter": "python", 632 | "pygments_lexer": "ipython3", 633 | "version": "3.6.8" 634 | } 635 | }, 636 | "nbformat": 4, 637 | "nbformat_minor": 2 638 | } 639 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | torch>=1.0.0 2 | tqdm 3 | requests 4 | -------------------------------------------------------------------------------- /satnet/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import SATNet 2 | 3 | __all__ = ['SATNet'] 4 | -------------------------------------------------------------------------------- /satnet/models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from torch.autograd import Function 4 | import torch.optim as optim 5 | 6 | import satnet._cpp 7 | if torch.cuda.is_available(): import satnet._cuda 8 | 9 | 10 | def get_k(n): 11 | return int((2*n)**0.5+3)//4*4 12 | 13 | class MixingFunc(Function): 14 | '''Apply the Mixing method to the input probabilities. 15 | 16 | Args: see SATNet. 17 | 18 | Impl Note: 19 | The SATNet is a wrapper for the MixingFunc, 20 | handling the initialization and the wrapping of auxiliary variables. 21 | ''' 22 | @staticmethod 23 | def forward(ctx, S, z, is_input, max_iter, eps, prox_lam): 24 | B, n, m, k = z.size(0), S.size(0), S.size(1), 32 #get_k(S.size(0)) 25 | ctx.prox_lam = prox_lam 26 | 27 | device = 'cuda' if S.is_cuda else 'cpu' 28 | ctx.g, ctx.gnrm = torch.zeros(B,k, device=device), torch.zeros(B,n, device=device) 29 | ctx.index = torch.zeros(B,n, dtype=torch.int, device=device) 30 | ctx.is_input = torch.zeros(B,n, dtype=torch.int, device=device) 31 | ctx.V, ctx.W = torch.zeros(B,n,k, device=device).normal_(), torch.zeros(B,k,m, device=device) 32 | ctx.z = torch.zeros(B,n, device=device) 33 | ctx.niter = torch.zeros(B, dtype=torch.int, device=device) 34 | 35 | ctx.S = torch.zeros(n,m, device=device) 36 | ctx.Snrms = torch.zeros(n, device=device) 37 | 38 | ctx.z[:] = z.data 39 | ctx.S[:] = S.data 40 | ctx.is_input[:] = is_input.data 41 | 42 | perm = torch.randperm(n-1, dtype=torch.int, device=device) 43 | 44 | satnet_impl = satnet._cuda if S.is_cuda else satnet._cpp 45 | satnet_impl.init(perm, is_input, ctx.index, ctx.z, ctx.V) 46 | 47 | for b in range(B): 48 | ctx.W[b] = ctx.V[b].t().mm(ctx.S) 49 | ctx.Snrms[:] = S.norm(dim=1)**2 50 | 51 | satnet_impl.forward(max_iter, eps, 52 | ctx.index, ctx.niter, ctx.S, ctx.z, 53 | ctx.V, ctx.W, ctx.gnrm, ctx.Snrms, ctx.g) 54 | 55 | return ctx.z.clone() 56 | 57 | @staticmethod 58 | def backward(ctx, dz): 59 | B, n, m, k = dz.size(0), ctx.S.size(0), ctx.S.size(1), 32 #get_k(ctx.S.size(0)) 60 | 61 | device = 'cuda' if ctx.S.is_cuda else 'cpu' 62 | ctx.dS = torch.zeros(B,n,m, device=device) 63 | ctx.U, ctx.Phi = torch.zeros(B,n,k, device=device), torch.zeros(B,k,m, device=device) 64 | ctx.dz = torch.zeros(B,n, device=device) 65 | 66 | ctx.dz[:] = dz.data 67 | 68 | satnet_impl = satnet._cuda if ctx.S.is_cuda else satnet._cpp 69 | satnet_impl.backward(ctx.prox_lam, 70 | ctx.is_input, ctx.index, ctx.niter, ctx.S, ctx.dS, ctx.z, ctx.dz, 71 | ctx.V, ctx.U, ctx.W, ctx.Phi, ctx.gnrm, ctx.Snrms, ctx.g) 72 | 73 | ctx.dS = ctx.dS.sum(dim=0) 74 | 75 | return ctx.dS, ctx.dz, None, None, None, None 76 | 77 | def insert_constants(x, pre, n_pre, app, n_app): 78 | ''' prepend and append torch tensors 79 | ''' 80 | one = x.new(x.size()[0],1).fill_(1) 81 | seq = [] 82 | if n_pre != 0: 83 | seq.append((pre*one).expand(-1, n_pre)) 84 | seq.append(x) 85 | if n_app != 0: 86 | seq.append((app*one).expand(-1, n_app)) 87 | r = torch.cat(seq, dim=1) 88 | r.requires_grad = False 89 | return r 90 | 91 | class SATNet(nn.Module): 92 | '''Apply a SATNet layer to complete the input probabilities. 93 | 94 | Args: 95 | n: Number of input variables. 96 | m: Rank of the clause matrix. 97 | aux: Number of auxiliary variables. 98 | 99 | max_iter: Maximum number of iterations for solving 100 | the inner optimization problem. 101 | Default: 40 102 | eps: The stopping threshold for the inner optimizaiton problem. 103 | The inner Mixing method will stop when the function decrease 104 | is less then eps times the initial function decrease. 105 | Default: 1e-4 106 | prox_lam: The diagonal increment in the backward linear system 107 | to make the backward pass more stable. 108 | Default: 1e-2 109 | weight_normalize: Set true to perform normlization for init weights. 110 | Default: True 111 | 112 | Inputs: (z, is_input) 113 | **z** of shape `(batch, n)`: 114 | Float tensor containing the probabilities (must be in [0,1]). 115 | **is_input** of shape `(batch, n)`: 116 | Int tensor indicating which **z** is a input. 117 | 118 | Outputs: z 119 | **z** of shape `(batch, n)`: 120 | The prediction probabiolities. 121 | 122 | Attributes: S 123 | **S** of shape `(n, m)`: 124 | The learnable clauses matrix containing `m` clauses 125 | for the `n` variables. 126 | 127 | Examples: 128 | >>> sat = satnet.SATNet(3, 4, aux=5) 129 | >>> z = torch.randn(2, 3) 130 | >>> is_input = torch.IntTensor([[1, 1, 0], [1,0,1]]) 131 | >>> pred = sat(z, is_input) 132 | ''' 133 | 134 | def __init__(self, n, m, aux=0, max_iter=40, eps=1e-4, prox_lam=1e-2, weight_normalize=True): 135 | super(SATNet, self).__init__() 136 | 137 | S_t = torch.FloatTensor(n+1+aux, m) # n+1 for truth vector 138 | S_t = S_t.normal_() 139 | if weight_normalize: S_t = S_t * ((.5/(n+1+aux+m))**0.5) 140 | 141 | self.S = nn.Parameter(S_t) 142 | self.aux = aux 143 | self.max_iter, self.eps, self.prox_lam = max_iter, eps, prox_lam 144 | 145 | def forward(self, z, is_input): 146 | B = z.size(0) 147 | device = 'cuda' if self.S.is_cuda else 'cpu' 148 | m = self.S.shape[1] 149 | if device == 'cpu' and m%4 != 0: 150 | raise ValueError('m is required to be a multiple of 4 on CPU for SSE acceleration. Now '+str(m)) 151 | is_input = insert_constants(is_input.data, 1, 1, 0, self.aux) 152 | z = torch.cat([torch.ones(z.size(0),1,device=device), z, torch.zeros(z.size(0),self.aux,device=device)],dim=1) 153 | 154 | z = MixingFunc.apply(self.S, z, is_input, self.max_iter, self.eps, self.prox_lam) 155 | 156 | return z[:,1:self.S.size(0)-self.aux] 157 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import torch.cuda 2 | 3 | from setuptools import setup 4 | from torch.utils.cpp_extension import BuildExtension, CppExtension, CUDAExtension 5 | from torch.utils.cpp_extension import CUDA_HOME 6 | 7 | gencode = [ 8 | '-gencode=arch=compute_50,code=sm_50', 9 | '-gencode=arch=compute_52,code=sm_52', 10 | '-gencode=arch=compute_60,code=sm_60', 11 | '-gencode=arch=compute_61,code=sm_61', 12 | '-gencode=arch=compute_75,code=sm_75', 13 | '-gencode=arch=compute_80,code=sm_80', 14 | '-gencode=arch=compute_86,code=sm_86', 15 | ] 16 | 17 | ext_modules = [ 18 | CppExtension( 19 | name = 'satnet._cpp', 20 | include_dirs = ['./src'], 21 | sources = [ 22 | 'src/satnet.cpp', 23 | 'src/satnet_cpu.cpp', 24 | ], 25 | extra_compile_args = ['-fopenmp', '-msse4.1', '-Wall', '-g'] 26 | ) 27 | ] 28 | 29 | if torch.cuda.is_available() and CUDA_HOME is not None: 30 | extension = CUDAExtension( 31 | name = 'satnet._cuda', 32 | include_dirs = ['./src'], 33 | sources = [ 34 | 'src/satnet.cpp', 35 | 'src/satnet_cuda.cu', 36 | ], 37 | extra_compile_args = { 38 | 'cxx': ['-DMIX_USE_GPU', '-g'], 39 | 'nvcc': ['-g', '-restrict', '-maxrregcount', '32', '-lineinfo', '-Xptxas=-v'] 40 | } 41 | ) 42 | ext_modules.append(extension) 43 | 44 | with open("README.md", "r", encoding="utf-8") as fh: 45 | long_description = fh.read() 46 | 47 | # Python interface 48 | setup( 49 | name='satnet', 50 | version='0.1.4', 51 | install_requires=['torch>=1.3'], 52 | packages=['satnet'], 53 | ext_modules=ext_modules, 54 | cmdclass={'build_ext': BuildExtension}, 55 | author='Po-Wei Wang', 56 | author_email='poweiw@cs.cmu.edu', 57 | url='https://github.com/locuslab/SATNet', 58 | zip_safe=False, 59 | description='Bridging deep learning and logical reasoning using a differentiable satisfiability solver', 60 | long_description=long_description, 61 | long_description_content_type="text/markdown", 62 | classifiers=[ 63 | "License :: OSI Approved :: MIT License", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /src/satnet.cpp: -------------------------------------------------------------------------------- 1 | #ifdef MIX_USE_GPU 2 | #include 3 | #endif 4 | #include 5 | 6 | #ifdef MIX_USE_GPU 7 | #define DEVICE_NAME cuda 8 | #define _MIX_DEV_STR "cuda" 9 | #define _MIX_CUDA_DECL , cudaStream_t stream 10 | #define _MIX_CUDA_ARG , stream 11 | #define _MIX_CUDA_HEAD cudaStream_t stream = at::cuda::getCurrentCUDAStream(); 12 | #define _MIX_CUDA_TAIL AT_CUDA_CHECK(cudaGetLastError()); 13 | //AT_CUDA_CHECK(cudaStreamSynchronize(stream)); 14 | #else 15 | #define DEVICE_NAME cpu 16 | #define _MIX_DEV_STR "cpu" 17 | #define _MIX_CUDA_DECL 18 | #define _MIX_CUDA_ARG 19 | #define _MIX_CUDA_HEAD 20 | #define _MIX_CUDA_TAIL 21 | #endif 22 | 23 | // name mangling for CPU and CUDA 24 | #define _MIX_CAT(x,y) x ## _ ## y 25 | #define _MIX_EVAL(x,y) _MIX_CAT(x,y) 26 | #define _MIX_FUNC(name) _MIX_EVAL(name, DEVICE_NAME) 27 | 28 | #include "satnet.h" 29 | 30 | using Tensor=torch::Tensor; 31 | float *fptr(Tensor& a) { return a.data_ptr(); } 32 | int *iptr(Tensor& a) { return a.data_ptr(); } 33 | 34 | void _MIX_FUNC(mix_init_launcher) (mix_t mix, int32_t *perm _MIX_CUDA_DECL); 35 | void _MIX_FUNC(mix_forward_launcher) (mix_t mix, int max_iter, float eps _MIX_CUDA_DECL); 36 | void _MIX_FUNC(mix_backward_launcher)(mix_t mix, float prox_lam _MIX_CUDA_DECL); 37 | 38 | void mix_init(Tensor perm, 39 | Tensor is_input, Tensor index, Tensor z, Tensor V) 40 | { 41 | _MIX_CUDA_HEAD; 42 | 43 | mix_t mix; 44 | mix.b = V.size(0); mix.n = V.size(1); mix.k = V.size(2); 45 | mix.is_input = iptr(is_input); 46 | mix.index = iptr(index); 47 | mix.z = fptr(z); 48 | mix.V = fptr(V); 49 | 50 | _MIX_FUNC(mix_init_launcher)(mix, iptr(perm) _MIX_CUDA_ARG); 51 | 52 | _MIX_CUDA_TAIL; 53 | } 54 | 55 | void mix_forward(int max_iter, float eps, 56 | Tensor index, Tensor niter, Tensor S, Tensor z, Tensor V, Tensor W, Tensor gnrm, Tensor Snrms, Tensor cache) 57 | { 58 | _MIX_CUDA_HEAD; 59 | 60 | mix_t mix; 61 | mix.b = V.size(0); mix.n = V.size(1); mix.m = S.size(1); mix.k = V.size(2); 62 | mix.index = iptr(index); 63 | mix.niter = iptr(niter); 64 | mix.S = fptr(S); 65 | mix.z = fptr(z); 66 | mix.V = fptr(V); 67 | mix.W = fptr(W); 68 | mix.gnrm = fptr(gnrm); mix.Snrms = fptr(Snrms); 69 | mix.cache = fptr(cache); 70 | 71 | _MIX_FUNC(mix_forward_launcher)(mix, max_iter, eps _MIX_CUDA_ARG); 72 | 73 | _MIX_CUDA_TAIL; 74 | } 75 | 76 | void mix_backward(float prox_lam, 77 | Tensor is_input, Tensor index, Tensor niter, Tensor S, Tensor dS, Tensor z, Tensor dz, 78 | Tensor V, Tensor U, Tensor W, Tensor Phi, Tensor gnrm, Tensor Snrms, Tensor cache) 79 | { 80 | _MIX_CUDA_HEAD; 81 | 82 | mix_t mix; 83 | mix.b = V.size(0); mix.n = V.size(1); mix.m = S.size(1); mix.k = V.size(2); 84 | mix.is_input = iptr(is_input); 85 | mix.index = iptr(index); 86 | mix.niter = iptr(niter); 87 | mix.S = fptr(S); mix.dS = fptr(dS); 88 | mix.z = fptr(z); mix.dz = fptr(dz); 89 | mix.V = fptr(V); mix.U = fptr(U); 90 | mix.W = fptr(W); mix.Phi = fptr(Phi); 91 | mix.gnrm = fptr(gnrm); mix.Snrms = fptr(Snrms); 92 | mix.cache = fptr(cache); 93 | 94 | _MIX_FUNC(mix_backward_launcher)(mix, prox_lam _MIX_CUDA_ARG); 95 | 96 | _MIX_CUDA_TAIL; 97 | } 98 | 99 | PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { 100 | m.def("init" , &mix_init, "SATNet init (" _MIX_DEV_STR ")"); 101 | m.def("forward" , &mix_forward, "SATNet forward (" _MIX_DEV_STR ")"); 102 | m.def("backward" , &mix_backward, "SATNet backward (" _MIX_DEV_STR ")"); 103 | } 104 | -------------------------------------------------------------------------------- /src/satnet.h: -------------------------------------------------------------------------------- 1 | typedef struct mix_t { 2 | int b, n, m, k; 3 | int32_t *is_input; // b*n 4 | int32_t *index; // b*n 5 | int32_t *niter; // b 6 | float *S, *dS; // n*m 7 | float *z, *dz; // b*n 8 | float *V, *U; // b*n*k 9 | float *W, *Phi; // b*m*k 10 | float *gnrm, *Snrms;// b*n 11 | float *cache; 12 | } mix_t ; 13 | -------------------------------------------------------------------------------- /src/satnet_cpu.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include "satnet.h" 14 | 15 | #define saxpy mysaxpy 16 | #define scopy myscopy 17 | #define sscal mysscal 18 | #define sdot mysdot 19 | #define snrm2 mysnrm2 20 | #define szero myszero 21 | #define saturate mysaturate 22 | 23 | const double MEPS = 1e-24; 24 | 25 | /* 26 | * Helper functions 27 | */ 28 | void saxpy(float *__restrict__ y, float a, const float *__restrict__ x, int l) 29 | { 30 | y = (float*)__builtin_assume_aligned(y, 4*sizeof(float)); 31 | x = (float*)__builtin_assume_aligned(x, 4*sizeof(float)); 32 | __m128 const a_ = _mm_set1_ps(a); 33 | for(int i=0; i1)*(1-x); 162 | } 163 | 164 | // consider the \min unsat problem, 165 | void mix_forward(int max_iter, float eps, int n, int m, int k, const int32_t *index, int32_t *niter, const float *S, float *z, float *V, float *W, float *gnrm, float *Snrms, float *cache) 166 | { 167 | float delta; 168 | int iter = 0; 169 | for (; iter < max_iter; iter++) { 170 | delta = mix_kernel(1, 0, m, k, index, S, NULL, V, NULL, W, gnrm, Snrms, cache); 171 | if (iter && delta < eps) break; 172 | if (iter == 0) eps = delta*eps; 173 | } 174 | 175 | *niter = iter; 176 | 177 | for (int i,i_=0; (i=index[i_]); i_++) { 178 | float zi = V[i*k]; 179 | zi = saturate((zi+1)/2)*2-1; 180 | zi = saturate(1-acosf(zi)/M_PI); 181 | z[i] = zi; 182 | } 183 | } 184 | 185 | void mix_backward(float prox_lam, int n, int m, int k, int32_t *is_input, int32_t *index, int32_t *niter, const float *S, float *dS, float *z, float *dz, const float *V, float *U, float *W, float *Phi, float *gnrm, float *Snrms, float *cache) 186 | { 187 | int invalid_flag=0; 188 | for (int i,i_=0; (i=index[i_]); i_++) { 189 | float zi = z[i]; 190 | float dzi = dz[i]/M_PI/sin(zi*M_PI); 191 | if (isnan(dzi) || isinf(dzi) || gnrm[i] < MEPS) invalid_flag = 1; 192 | dz[i] = dzi; 193 | } 194 | if (invalid_flag) { szero(dz, n); return; } 195 | 196 | // solve P (S'S+D_z-D_sii)xI_k P U = -dz P v0 197 | for (int iter=0; iter<*niter; iter++) { 198 | mix_kernel(0, prox_lam, m, k, index, S, dz, U, V, Phi, gnrm, Snrms, cache); 199 | } 200 | 201 | // sanity check 202 | for (int ik=0; ik 2 | #include 3 | //#include 4 | #include 5 | #include 6 | 7 | #include 8 | #include "satnet.h" 9 | 10 | const double MEPS = 1e-24; 11 | const int WARP_SIZE = 32; 12 | const int WARP_NUM = 32; 13 | const int MBUF_SIZE = 320; 14 | 15 | // Warp level dot product 16 | __device__ 17 | float warpdot(const float * x, const float * z, int k) 18 | { 19 | if (k==0) return 0; 20 | int lane = threadIdx.x % WARP_SIZE; 21 | 22 | float val = 0; 23 | #pragma unroll 2 24 | for (int i=lane; iMBUF_SIZE ? MBUF_SIZE : m; // mbuf = # of m inside buffer (in smem) 90 | int mrem = m>MBUF_SIZE ? m-MBUF_SIZE : 0; // mrem = # of m outside buffer (in global mem) 91 | for (int j=lane; j>>(perm, 259 | mix.n, mix.k, mix.is_input, mix.index, mix.z, 260 | mix.V); 261 | } 262 | 263 | void mix_forward_launcher_cuda(mix_t mix, int max_iter, float eps, cudaStream_t stream) 264 | { 265 | int smem_size = (mix.m+mix.k*(1+MBUF_SIZE))*sizeof(float); 266 | mix_forward<<>>(max_iter, eps, 267 | mix.n, mix.m, mix.k, mix.index, mix.niter, 268 | mix.S, mix.z, mix.V, mix.W, mix.gnrm, mix.Snrms, mix.cache); 269 | } 270 | 271 | void mix_backward_launcher_cuda(mix_t mix, float prox_lam, cudaStream_t stream) 272 | { 273 | int smem_size = (mix.m+mix.k*(1+MBUF_SIZE))*sizeof(float); 274 | mix_backward<<>>(prox_lam, 275 | mix.n, mix.m, mix.k, mix.is_input, mix.index, mix.niter, 276 | mix.S, mix.dS, mix.z, mix.dz, mix.V, mix.U, mix.W, mix.Phi, mix.gnrm, mix.Snrms, mix.cache); 277 | } 278 | --------------------------------------------------------------------------------