diff --git a/benchmarks/graph_network.ipynb b/benchmarks/graph_network.ipynb index f06bb8079..1171d57da 100644 --- a/benchmarks/graph_network.ipynb +++ b/benchmarks/graph_network.ipynb @@ -56,7 +56,8 @@ " 'atom_filter': -1,\n", " 'prior_model': None,\n", " 'output_model': 'Scalar',\n", - " 'reduce_op': 'add'\n", + " 'reduce_op': 'add',\n", + " 'neighbors': 'simple'\n", "})\n", "\n", "# Graph network (compatible with NNPOps, https://github.com/torchmd/torchmd-net/issues/48),\n", @@ -79,7 +80,31 @@ " 'atom_filter': -1,\n", " 'prior_model': None,\n", " 'output_model': 'Scalar',\n", - " 'reduce_op': 'add'\n", + " 'reduce_op': 'add',\n", + " 'neighbors': 'simple'\n", + "})\n", + "\n", + "# Graph network (brute force)\n", + "model_3 = create_model({\n", + " 'embedding_dimension': 128,\n", + " 'num_layers': 6,\n", + " 'num_rbf': 50,\n", + " 'rbf_type': 'expnorm',\n", + " 'trainable_rbf': True,\n", + " 'activation': 'silu',\n", + " 'neighbor_embedding': True,\n", + " 'cutoff_lower': 0.0,\n", + " 'cutoff_upper': 5.0,\n", + " 'max_z': 100,\n", + " 'max_num_neighbors': 32,\n", + " 'model': 'graph-network',\n", + " 'aggr': 'add',\n", + " 'derivative': False,\n", + " 'atom_filter': -1,\n", + " 'prior_model': None,\n", + " 'output_model': 'Scalar',\n", + " 'reduce_op': 'simple_add',\n", + " 'neighbors': 'brute_force'\n", "})" ] }, @@ -96,7 +121,7 @@ "metadata": {}, "outputs": [], "source": [ - "def benchmark(model, pdb_file, device, optimize=True, compute_forces=True, compute_derivatives=False, batch_size=1):\n", + "def benchmark(model, pdb_file, device, optimize=True, compute_forces=True, compute_derivatives=False, batch_size=1, cuda_graph=False):\n", "\n", " # Optimize the model\n", " model = deepcopy(model).to(device)\n", @@ -123,11 +148,34 @@ " assert not (compute_forces and (batch_size > 1))\n", " positions.requires_grad = compute_forces\n", "\n", + " # Setup a benchmark\n", + " stmt = None\n", + " graph = None\n", + " if cuda_graph:\n", + "\n", + " # Create a graph\n", + " graph = pt.cuda.CUDAGraph()\n", + "\n", + " # Warm up the graph\n", + " for _ in range(3):\n", + " energy = model(atomic_numbers, positions, batch)[0]\n", + " if compute_forces or compute_derivatives:\n", + " energy.sum().backward()\n", + "\n", + " # Capture the grpah\n", + " with pt.cuda.graph(graph):\n", + " energy = model(atomic_numbers, positions, batch)[0]\n", + " if compute_forces or compute_derivatives:\n", + " energy.sum().backward()\n", + "\n", + " stmt = 'graph.replay()'\n", + " else:\n", + " stmt = f'''\n", + " energy = model(atomic_numbers, positions, batch)\n", + " {'energy[0].sum().backward()' if compute_forces or compute_derivatives else ''}\n", + " '''\n", + "\n", " # Benchmark\n", - " stmt = f'''\n", - " energy = model(atomic_numbers, positions, batch)\n", - " {'energy[0].sum().backward()' if compute_forces or compute_derivatives else ''}\n", - " '''\n", " timer = Timer(stmt=stmt, globals=locals())\n", " speed = timer.blocked_autorange(min_run_time=10).median * 1000 # s --> ms\n", "\n", @@ -151,45 +199,65 @@ "output_type": "stream", "text": [ "Method: default\n", - " ALA2: 7.852263981476426 ms/it\n", - " CLN: 8.225349669810385 ms/it\n", - " DHFR: 27.21661669202149 ms/it\n", - " FC9: 65.51229511387646 ms/it\n", + " ALA2: 7.977965270001733 ms/it\n", + " TST: 7.618675989997428 ms/it\n", + " CLN: 6.1609561600016605 ms/it\n", + " DHFR: 27.396543099985138 ms/it\n", + " FC9: 66.3056460007283 ms/it\n", " STMV: failed\n", "Method: compatible\n", - " ALA2: 7.383093530079351 ms/it\n", - " CLN: 7.977409199811517 ms/it\n", - " DHFR: 25.64150399994105 ms/it\n", - " FC9: 62.23246781155467 ms/it\n", + " ALA2: 7.658607015000597 ms/it\n", + " TST: 7.792522695003754 ms/it\n", + " CLN: 7.312152670001524 ms/it\n", + " DHFR: 25.790021100056038 ms/it\n", + " FC9: 62.54085699993084 ms/it\n", " STMV: failed\n", "Method: optimized\n", - " ALA2: 2.734545150306076 ms/it\n", - " CLN: 3.929289639927447 ms/it\n", - " DHFR: 20.75393449049443 ms/it\n", - " FC9: 47.54591805394739 ms/it\n", - " STMV: 217.71628607530147 ms/it\n" + " ALA2: 3.787452119995578 ms/it\n", + " TST: 2.829501859996526 ms/it\n", + " CLN: 2.9367655650003144 ms/it\n", + " DHFR: 20.309613449990138 ms/it\n", + " FC9: 47.33222264999313 ms/it\n", + " STMV: 222.8190639998502 ms/it\n", + "Method: brute_force\n", + " ALA2: 6.479403905000254 ms/it\n", + " TST: 5.038708424995093 ms/it\n", + " CLN: 8.716471800016734 ms/it\n", + " DHFR: failed\n", + " FC9: failed\n", + " STMV: failed\n", + "Method: brute_force+graph\n", + " ALA2: 1.1300752100032696 ms/it\n", + " TST: 1.5762312850029048 ms/it\n", + " CLN: 8.712789699984569 ms/it\n", + " DHFR: failed\n", + " FC9: failed\n", + " STMV: failed\n" ] } ], "source": [ "device = pt.device('cuda')\n", "systems = [('systems/alanine_dipeptide.pdb', 'ALA2'),\n", + " ('systems/testosterone.pdb', 'TST'),\n", " ('systems/chignolin.pdb', 'CLN'),\n", " ('systems/dhfr.pdb', 'DHFR'),\n", " ('systems/factorIX.pdb', 'FC9'),\n", " ('systems/stmv.pdb', 'STMV')]\n", "\n", - "methods = [('default', model_1, False),\n", - " ('compatible', model_2, False),\n", - " ('optimized', model_2, True)]\n", + "methods = [('default', model_1, False, False),\n", + " ('compatible', model_2, False, False),\n", + " ('optimized', model_2, True, False),\n", + " ('brute_force', model_3, False, False),\n", + " ('brute_force+graph', model_3, False, True)]\n", "\n", "speed_methods = {}\n", - "for meth, model, optimize in methods:\n", + "for meth, model, optimize, cuda_graph in methods:\n", " speed_methods[meth] = {}\n", " print(f'Method: {meth}')\n", " for pdb_file, name in systems:\n", " try:\n", - " speed = benchmark(model, pdb_file, device, optimize=optimize, compute_forces=True, compute_derivatives=False, batch_size=1)\n", + " speed = benchmark(model, pdb_file, device, optimize=optimize, compute_forces=True ,compute_derivatives=False, batch_size=1, cuda_graph=cuda_graph)\n", " speed_methods[meth][name] = speed\n", " print(f' {name}: {speed} ms/it')\n", " except Exception as e:\n", @@ -203,7 +271,7 @@ "outputs": [ { "data": { - "image/png": "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W7cuL7zwAgDuzocffrjbvj/88AOHHHIIWVlZfPvtt7z66qvxddnZ2cybNw+AF198sdjXPuyww9iwYUMKzkpERCo7JfRy1rx5cy6//HKaNm1Kly5daNeuHQDPPvssTz75JE2aNKFhw4ZMnDhxt32bNGlCs2bNaNiwIb1796Zt27bxdXfddRe///3vadeuXYlTq3br1o0HHniAZs2aaVCciMh+RtOnyl5TXYtIlGn6VBEREalwSugiIiIRoIQuIiISAUroIiIiEaCELiIiEgFK6CIiIhGghB5BRadRvfPOO5k6dSpQeJKXRHua5lVERCq36D/6dUBWOR8vr3yPlwJFp1G955570hyRiIikmlroKfDMM8+Qk5NDkyZN+NWvfsUXX3xB+/btycnJoX379nz55ZcA9OzZk759+3L22Wdz4oknMnPmTHr37s2pp55Kz54948c79NBD+eMf/0jz5s1p3749a9asAWDYsGG0atWKJk2a0KVLF/Lz84udRrVnz56MHTs2frwHHniA1q1b07p1az777LPd4i9uqlcREanclNDL2eLFi7n33nuZNm0aH374IQ899BD9+vXjyiuvZMGCBVxxxRVcf/318e2///57pk2bxoMPPkinTp248cYbWbx4MQsXLoxP5rJp0yaaN2/O+++/z1lnncXdd98NwCWXXMKcOXP48MMPOfXUU3nyySfj06g+8MADzJ8/n5NOOmm3GGvWrMl7771Hv379uOGGG3Zb36dPHx555BHmzZvH4MGDufbaa1NSVyIiUn6i3+VewaZNm0bXrl2pVasWAEceeSTvvPMO48aNA+BXv/oVt9xyS3z7Tp06YWY0btyYY445hsaNg0cNNmzYkBUrVtC0aVOqVKnC5ZdfDkCPHj245JJLAFi0aBG3334769evZ+PGjZx//vlJxdi9e/f4/zfeeGOhdRs3bkx6qlcREak8lNDLmbvvNn1qUYnrDzroIACqVKkS/7mgvH379lL379mzJxMmTKBJkyaMGDGCGTNmJBVj4uvv7VSvIiJSuajLvZy1b9+e559/nrVr1wKwbt06Tj/9dEaPHg0Es66dccYZZTrmzp074/fAn3vuufj+GzZs4Nhjj2Xbtm08++yz8e33NI3qmDFj4v+fdtpphdbVrFkzqaleRUSkclELvZw1bNiQ2267jbPOOouqVavSrFkzHn74YXr3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RESkUkmmhf5b4DrgeOArgpnWrkthTCIiIlJGyYxyzwWuqIBYREREZC8lM8r9RDObZGZrzOw7M5toZidWRHAiIiKSnGS63J8DngeOBY4DXgBGpTIoERERKZtkErq5+7/dfXv4byTgqQ5MREREkpfMKPfpZtYfGE2QyC8HXjGzIwHcfV0K4xMREZEkJJPQLw///02R5b0JErzup4uIiKRZMqPc61ZEICIiIrL3SryHbmatzOzHCeUrwxHuDxd0t4uIiEjlUNqguH8C/wMwszOBQcAzBNOnDk19aCIiIpKs0rrcqyYMeLscGOruLwIvmtn8lEcmIiIiSSuthV7VzAoSfntgWsK6ZAbTiYiISAUpLTGPAmaaWS6wGZgFYGYnE3S7i4iISCVRYkJ393vN7A2CJ8S95u4FD5OpAvyuIoITERGR5JTade7u/y1m2SepC0dERET2RjKPfhUREZFKTgldREQkApTQRUREIkAJXUREJAKU0EVERCJACV1ERCQClNBFREQiQAldREQkApTQRUREIkAJXUREJAKU0EVERCJACV1ERCQClNBFREQiQAldREQkApTQRUREIkAJXUREJAKU0EVERCJACV1ERCQClNBFREQiQAldREQkApTQRUREIqDSJHQzO9HMnjSzsemORUREJNOkNKGb2VNm9p2ZLSqyvIOZLTWzz8ysP4C7L3f3X6cyHhERkahKdQt9BNAhcYGZVQX+AXQEGgDdzaxBiuMQERGJtJQmdHd/E1hXZHFr4LOwRf4/YDRwUbLHNLM+ZjbXzOauWbOmHKMVERHJXOm4h348sDKhvAo43syOMrMngGZmdmtJO7v7UHdv6e4tjz766FTHKiIikhEOSMNrWjHL3N3XAr+t6GBERESiIB0t9FVAnYRybeDrNMQhIiISGelI6HOAemZW18wOBLoBL6UhDhERkchI9dfWRgHvAKeY2Soz+7W7bwf6Af8BPgaed/fFqYxDREQk6lJ6D93du5ewfDIwOZWvLSIisj+pNE+KKwsz62RmQ/Py8tIdioiISKWQkQnd3Se5e5+srKx0hyIiIlIpZGRCFxERkcKU0EVERCJACV1ERCQClNBFREQiIB2PfpX9THb/V8rlOCsGXVAuxxERiSK10EVERCIgI1voZtYJ6HTyySenOxRJpwHl8LXFAXqWgYhEQ0a20PU9dBERkcIysoWe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", + "image/png": "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gHroIiIiAaCELiIiEgAJfVFc9erVXXJycrzDEBERKRHLli3b7pw7pvYDJHhCT05OZunSpfEOQ0REpESYWZ4P/tGQu4iISAAooYuIiASAErqIiEgAJPQ59NwcPHiQjRs3sn+/nl5YllWqVImaNWtSoUKFeIciIlIiApfQN27cyMknn0xycjJR1gSWgHHOsWPHDjZu3EitWrXiHY6ISIkI3JD7/v37Of3005XMyzAz4/TTT9cojYiUKYFL6ICSuegYEJEyJ5AJPd4GDBjAmWeeSYMGDbJN37lzJ1dddRV16tThqquu4scffyyW7W3evJmrr776mOn9+vXjnXfeKZZtiIhI6aaEHgP9+vVj5syZx0wfPnw47du356uvvqJ9+/YMHz68WLY3c+ZMOnToUCzrEhGRxKSEHgNt2rThtNNOO2b61KlTufnmmwG4+eabmTJlyjHzzJ07l1AoRI8ePahbty433XQTWRXxhg4dSr169UhJSeHee+8NLzNz5kw6deqEc44hQ4ZQr149rr32WrZu3Rqe57HHHqN58+Y0aNCAgQMH4pxj7dq1NGnSJDzPV199RdOmTYtrN4iISAkKfEIPhUKMHTsW8G5pC4VCjBs3DoDMzExCoRATJ04EICMjg1AoxKRJkwDYvn07oVCI6dOnA7Bly5bjiuWHH36gRo0aANSoUSNbwo302WefMXLkSD7//HPWrVvHf//7X3bu3MnkyZNZvXo1K1as4E9/+hMAhw8fJj09nXr16jF58mTS09NZuXIlo0ePZtGiReF1DhkyhCVLlrBq1Sr27dvHe++9x4UXXkhSUhJpaWkAjBkzhn79+h3XexQRkfgIfEJPRC1atKBmzZqUK1eO1NRU1q9fT7Vq1ahUqRK33HILkyZNokqVKgB88skntGzZEoD58+fTu3dvypcvz9lnn027du3C65wzZw4tW7akYcOGzJ49m9WrVwNwyy23MGbMGA4fPszEiRP51a9+VfJvWEREjlvg7kPPae7cueGfK1SokK1dpUqVbO2kpKRs7erVq2dr/+IXvziuWM466yw2b95MjRo12Lx5M2eeeWau85144onhn8uXL8+hQ4c44YQTWLx4MR999BETJkzg+eefZ/bs2bz//vt07NgxPH9uV3fv37+fO+64g6VLl3LuuecybNiw8C1d3bt359FHH6Vdu3Y0bdqU008//bjeo4hIWdHw1YYFzrPy5pUlEIlHPfQS1LVrV1599VUAXn31VX75y19GveyePXvIyMjgmmuuYeTIkeFh8o8++oj27dsD3rn7CRMmcPjwYTZv3sycOXMAwsm7evXq7NmzJ9uV75UqVaJDhw4MGjSI/v37F8fbFBGROFBCj4HevXtz6aWXkp6eTs2aNXn55ZcB76K2Dz/8kDp16vDhhx8ydOjQqNe5e/duOnfuTEpKCm3btuXpp59m27ZtVKpUiWrVqgHQrVs36tSpQ8OGDRk0aBBt27YF4JRTTuHWW2+lYcOGXHfddTRv3jzbum+66SbMLNdb30REJDFY1hXUiahZs2YuZz30L774gosvvjhOEZWscePGsXHjxkJ9McjNiBEjyMjI4PHHHy+myEqHsnQsiEjJi8eQu5ktc841y+21wJ9DD7I+ffoc9zq6devG2rVrmT17djFEJCIi8aKEXsZNnjw53iGIiEgx0Dl0ERGRAEjIhG5mXcxsVEZGRrxDERERKRUSMqE756Y75wYmJSXFOxQREZFSISETuoiIiGSnhB4DRSmf+uSTT1K7dm0uuugiPvjgg2KLpWPHjnz//ffZps2dO5fOnTsX2zZERCT+lNBjoLDlUz///HMmTJjA6tWrmTlzJnfccQeHDx8+7jj27dvHzp07Oeecc457XSIiUropocdAYcunTp06lV69enHiiSdSq1YtateuzeLFi49ZPjk5mUceeYQmTZrQsGFD1qxZA8C8efNITU0lNTWVxo0bs3v3buBoKVbwSqzWrVuXyy+/PFxNDmDx4sVcdtllNG7cmMsuu4z09HQAWrduHX68LECrVq1YsWLFce8bERGJjeAn9FAI/PKpHDzotf3yqWRmem2/fCoZGV47K+Ft3+61/fKpxKh86vfff8+5554bnq9mzZrHDJNnqV69Op9++imDBg1ixIgRgPektxdeeIG0tDQWLFhA5cqVAcKFW/bv38+tt97K9OnTWbBgQbYysHXr1mX+/Pl89tlnPPbYYzzwwAOAV4Utq+zsl19+yYEDB0hJSTmu9y8iIrET/ISeAHJ7/G5uVdMArr/+egCaNm3K+vXrAa/3/Lvf/Y5nn32WXbt2ccIJ3vOC/vvf/3L55ZezZs0aatWqRZ06dTCzbE+Yy8jI4IYbbqBBgwbcc8894bKqN9xwA++99x4HDx7klVdeUZ10EZFSLvhPiosof0qFCtnbVapkbyclZW9Xr569HaPyqTVr1mTDhg3h+TZu3MjZZ5+d6zqySqtmlVUFr+jLtddey4wZM7jkkkuYNWsWFStW5Nxzz6VixYpA3l8QHnroIa644gomT57M+vXrw0P0VapU4aqrrmLq1Km89dZb5HxmvoiIlC7qoZegvMqndu3alQkTJnDgwAG++eYbvvrqK1q0aBH1eteuXUvDhg354x//SLNmzVizZk22Oul169blm2++Ye3atQCMHz8+vGxGRkb4ormsIfYst9xyC3fddRfNmzfP9ZoAEREpPZTQY6Cw5VPr16/PjTfeSL169ejYsSMvvPAC5cuXj3p7I0eOpEGDBjRq1IjKlSvTqVMnZs6cGU7olSpVYtSoUVx77bVcfvnlnH/++eFl//CHP3D//ffTqlWrY66sb9q0KdWqVVOddBGRBKDyqQF04MABWrVqddzD5Js2bSIUCrFmzRrKlUu87346FkQklkpb+dTE+5SWAp144onHncxfe+01WrZsyRNPPJGQyVxEpKwJ/kVxUiR9+/alb9++8Q5DRESipK6XiIhIACihi4iIBIASuoiISAAooYuIiASAEnoJGDZsWPi567nZtm0bLVu2pHHjxixYsKDQ6x87dixDhgwBYMqUKXz++edFjlVERBJT4K9yTx7672Jd3/rh1xbr+gA++ugj6tatG36K3PGYMmUKnTt3pl69esUQmYiIJAr10GPkiSee4KKLLuLKK68MlyRdu3YtHTt2pGnTprRu3Zo1a9aQlpbGH/7wB2bMmEFqair79u1j0KBBNGvWjPr16/PII4+E15mcnMz27dsBWLp0afi561kWLVrEtGnTuO+++0hNTQ0/6lVERIIv8D30eFi2bBkTJkzgs88+49ChQzRp0oSmTZsycOBAXnrpJerUqcMnn3zCHXfcwezZs3nsscdYunQpzz//POB9GTjttNM4fPgw7du3Z8WKFVGVLr3sssvo2rUrnTt3pkePHrF+myIiUoooocfAggUL6NatG1WqVAG84iv79+9n0aJF3HDDDeH5Dhw4kOvyb731FqNGjeLQoUNs3ryZzz//XLXIRUQkX0roMZKzXOmRI0c45ZRTSEtLy3e5b775hhEjRrBkyRJOPfVU+vXrx/79+wE44YQTOHLkCEB4moiICOgceky0adOGyZMns2/fPnbv3s306dOpUqUKtWrV4u233wbAOcfy5cuPWfann37ipJNOIikpiR9++IH3338//FpycjLLli0D4N1338112yeffDK7d++OwbsSEZHSTAk9Bpo0aULPnj1JTU2le/futG7dGoA33niDl19+mUaNGlG/fn2mTp16zLKNGjWicePG1K9fnwEDBtCqVavwa4888gi//e1vad26dZ7lVXv16sVTTz1F48aNdVGciEgZovKpElg6FkQkllQ+VURERIqdErqIiEgAKKGLiIgEgBK6iIhIACihi4iIBIASuoiISAAooQdUzjKqDz/8MLNmzQKyF3mJVFCZVxERKb2C/+jXYUnFvL6M4l1fjOQso/rYY4/FOSIREYmlUtVDN7PrzGy0mU01s6vjHc/xeO2110hJSaFRo0b8+te/5ttvv6V9+/akpKTQvn17vvvuOwD69evHoEGDuOKKK7jggguYN28eAwYM4OKLL6Zfv37h9VWtWpXf//73NGnShPbt27Nt2zYARo8eTfPmzWnUqBHdu3cnMzMz1zKq/fr145133gmv76mnnqJFixa0aNGCr7/++pj4cyv1KiIipVfME7qZvWJmW81sVY7pHc0s3cy+NrOhAM65Kc65W4F+QM9YxxYrq1ev5oknnmD27NksX76cZ555hiFDhtC3b19WrFjBTTfdxF133RWe/8cff2T27Nk8/fTTdOnShXvuuYfVq1ezcuXKcDGXvXv30qRJEz799FPatm3Lo48+CsD111/PkiVLWL58ORdffDEvv/xyuIzqU089RVpaGhdeeOExMVarVo3FixczZMgQ7r777mNeHzhwIM899xzLli1jxIgR3HHHHTHZVyIiUjxKooc+FugYOcHMygMvAJ2AekBvM6sXMcuf/NcT0uzZs+nRowfVq1cH4LTTTuPjjz/mV7/6FQC//vWvWbhwYXj+Ll26YGY0bNiQs846i4YNG1KuXDnq16/P+vXrAShXrhw9e3rfcfr06RNeftWqVbRu3ZqGDRvyxhtvsHr16qhi7N27d/j/jz/+ONtre/bsCZd6TU1N5bbbbmPz5s1F3yEiIhJzMT+H7pybb2bJOSa3AL52zq0DMLMJwC/N7AtgOPC+c+7T3NZnZgOBgQDnnXdezOI+Hs65Y8qn5hT5+oknngh4STvr56z2oUOH8l2+X79+TJkyhUaNGjF27Fjmzp0bVYyR2y9q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PN/ixYv529/+xsqVK1m7di2TJk0CYO/evVxyySUsX76cNm3aMHr06AJjEhEJOo13xkjOeujPPvssUPR66NOmTWPEiBEA4XroBT2nPLd66DfccEP49fzqoWdtd8WKFeFKcBkZGXz11VdUq1aN9u3bA7Bz505+/vnncNGW119/ncOHD3PBBRdQq1YtwDunP2rUqPB2u3btSuXKlQE4ePAgQ4YMIS0tjfLly/Pll1+G52vRogUXXHBBeB0LFy6kR48eVKxYMdzjb9q0KR9++GGh9qeISBApocdI0Ouhp6WlAbmfQy+ovGvkPnj66ac566yzWL58OUeOHKFSpUrh1/LahxUqVAj/XL58eQ4dOpTv9kREygINucdIWa6HXrduXdatW8f69esBmDhxYp7zZmRkUKNGDcqVKxfu3WdZvHgx33zzDUeOHGHixIm57kMREfEEvocezW1msVCW66FXrlyZF198kY4dO1K9enVatGiR57x33HEH3bt35+233+aKK67I1nu/9NJLGTp0KCtXrgxfICciIrlTPXSJiT179lC1alWccwwePJg6deoU6p74uXPnMmLEiKi/uORGx4IEWazqoes+9OipHrqUCaNHjyY1NZX69euTkZHBbbfdFu+QREQCLSGH3M2sC9Cldu3a8Q4lbkp7PfR77rnnuJ5SFwqFCIVCxReQiEjAJWRCd85NB6Y3a9bs1jxez/Wq7SBRPfT8JfKpJBGRogjckHulSpXYsWOHPtDLMOccO3bsyHYLnIhI0CVkDz0/NWvWZOPGjcc8nUzKlkqVKlGzZs14hyEiUmICl9ArVKgQfkKZiIhIWRG4IXcREZGySAldREQkABI6oaenpzN27FjAK/IRCoUYN24cAJmZmYRCofBjRzMyMgiFQuGKXdu3bycUCjF9+nTAqyMeCoWYOXMmABs2bCAUCjFr1iwA1q1bRygUYt68eeFth0KhcLnPVatWEQqFwo9PTUtLIxQKhZ95vmTJEkKhEKtWrQJg0aJFhEIh0tPTAZg3bx6hUIh169YBMGvWLEKhEBs2bABg5syZhEIhtmzZAsD06dMJhUJs374dgEmTJhEKhcjI8B4OMXHiREKhEJmZmQCMGzeOUCgUfozr2LFjs90WNnr0aK688spw+8UXX6RTp07h9jPPPEPXrl3D7REjRtC9e/dwe/jw4dkqyD3++OP06dMn3H744Yfp379/uH3//fczcODAcPvee+9l8ODB4fbdd9/N3XffHW4PHjyYe++9N9weOHAg999/f7jdv39/Hn744XC7T58+PP744+F2r169GD58eLjdvXv3cLEb8ArGPPPMM+F2p06dwhXyAK688spsVd1CoZCOPR17QHyPPXf4EFveHMqe1XMAOHJwP1veHMreL+YDEcfeF96+3555hNDYvUxP99pb9njtmV979RA2bNjAuifXsWf1HgB+3voz655cx9413iOfD2w+wLon1+nY8217bxsbXtwQbm+dupUN/zza/mHSD8V+7OUnoRO6iIiIeAL36FcRkbJAj36NPz36VURERIqdErqIiEgAKKGLiIgEgBK6iIhIACihi4iIBIASuoiISAAkZEI3sy5mNirrYQIiIiJlXUImdOfcdOfcwKSkKO6tFBERKQMSMqGLiIhIdkroIiIiAZBnPXQzuz6K5fc752YUYzwiIiJSBHkmdGA0MBWwfOZpAyihi0ixKNTzyUUkm/wS+vvOuQH5LWxm44o5HhERESmCPM+hO+f6AJjZiTlfy5qWNY+IiIjEV3499CwfA02imCYiUnIKURJUpCzI76K4XwDnAJXNrDFHz6VXA6qUQGwiIiISpfx66B2AfkBN4O8R03cDD8QwJhERESmkPBO6c+5V4FUz6+6ce7cEYxIREZFCym/IvY9zbhyQbGa/y/m6c+7vuSwmIiIicZDfkHvWefKqJRGIiIiIFF1+CX23mZ3unHu0xKIRERGRIskvoZ8PvG1mFYCPgPeBxc45VyKRiYiISNTye7DMcOdcO+AaYDkwAPjUzN40s75mdlZJBSkiIiL5K/DBMs653cBk/x9mVg/oBLyGd2ubiIiIxFmB5VPNrJWZneT/3Ae4BXjHOadkLiIiUkpEUw/9H0CmmTUC/gB8i9c7FxERkVIimoR+yL8Q7pfAM865Z4CTYxuWiIiIFEY0xVl2m9n9QB+gjZmVByrENiwREREpjGgSek/gV8BvnHNbzOw84KnYhiUiUnwavtqwwHlW3ryyBCIRiZ0Ch9ydc1v8x7wuN7PTgD3AezGPLB9m1sXMRmVkqDSiiIgIRHeV+21m9gOwAljm/1sa68Dy45yb7pwbmJQURT1kERGRMiCaIfd7gfrOue2xDkZERESKJpqr3NcCmbEORERERIoumh76/cAiM/sEOJA10Tl3V8yiEhERkUKJJqH/E5gNrASOxDYcERERKYpoEvoh59zvYh6JiIiIFFk059DnmNlAM6thZqdl/Yt5ZCIiIhK1aHrov/L/vz9imgMuKP5wREREpCiiKZ9aqyQCERERkaLLc8jdzJoUtHA084iIiEjs5ddDH2NmIcDymedloHFxBiQiIiKFl19CT8J7zGt+CX1b8YYjIiIiRZFnQnfOJZdgHCIiInIcorltTUREREo5JXQREZEAUEIXEREJgDzPoRd0S5pz7tPiD0dERESKIr+r3P/m/18JaAYsx7viPQX4BLg8tqGJiIhItPIccnfOXeGcuwL4FmjinGvmnGuKd9/51yUVoIiIiBQsmnPodZ1zK7MazrlVQGrMIhIREZFCi6Y4yxdm9i9gHF5Rlj7AFzGNSkRERAolmoTeHxgE/NZvzwf+EbOIREREpNCiqba238xeAmY459JLICYREREppALPoZtZVyANmOm3U81sWozjEhERkUKI5qK4R4AWwC4A51wakByziERERKTQoknoh5xzGTGPRERERIosmoviVpnZr4DyZlYHuAtYFNuwREREpDCi6aHfCdQHDgBvAhnA3TGMSURERAopmqvcM4EHzewvzrm9JRCTiIiIFFI0V7lfZmaf4z9MxswamdmLMY9MREREohbNkPvTQAdgB4BzbjnQJpZBiYiISOFEVQ/dObchx6TDMYhFREREiiiaq9w3mNllgDOzinhXuetZ7iIiIqVIND3024HBwDnA93iV1gbHMCYREREppGiuct8O3BTrQMzsAuBBIMk51yPW2xMREQmSaK5yv8DMppvZNjPbamZT/eRbIDN7xV9mVY7pHc0s3cy+NrOhAM65dc653xTtbYiIiJRt0Qy5vwm8BdQAzgbeBsZHuf6xQMfICWZWHngB6ATUA3qbWb0o1yciIiK5iCahm3PudefcIf/fOMBFs3Ln3HxgZ47JLYCv/R75z8AE4JeFilpERESyiSahzzGzoWaWbGbnm9kfgH+b2WlmdloRtnkOEHkb3EbgHDM73a+73tjM7s9rYTMbaGZLzWzptm3birB5ERGR4InmtrWe/v+35Zg+AK+nHtX59AiWyzTnnNuBd0V9vpxzo4BRAM2aNYtqpEBERCToornKvVYxb3MjcG5EuyawqZi3ISIiUqbkOeRuZs3N7BcR7b7+Fe7PFnGoPcsSoI6Z1fIfVNMLmHYc6xMRESnz8juH/k/gZwAzawMMB17DK586KpqVm9l44GPgIjPbaGa/cc4dAoYAH+A9ce4t59zqor8FERERyW/IvbxzLusK9Z7AKOfcu8C7ZpYWzcqdc73zmD4DmFGYQEVERCRv+fXQy5tZVsJvD8yOeC2ai+lixsy6mNmojIyMeIYhIiJSauSXmMcD88xsO7APWABgZrXxht3jxjk3HZjerFmzW+MZh0gi+aLuxQXOc/Ear+7SC7fPLmBOGPxSu+OOSUSKT54J3Tn3hJl9hPeEuP8457JuESsH3FkSwYmIiEh08h06d879L5dpX8YuHBERESmKaJ4UJyIiIqWcErqIiEgAxPVq9aIysy5Al9q1axfrepOH/rvAedYPv7ZYtykiIlIcErKH7pyb7pwbmJSUFO9QRERESoWE7KFLghkWxRevYXqmgIjI8VBCPx5lOFHp9ISISOmSkEPuIiIikp166CKlSRke9RGR46MeuoiISAAooYuIiARAQiZ0VVsTERHJLiETuu5DFxERyS4hE7qIiIhkp4QuIiISALptrRT6ou7FBc5z8ZovSiASERFJFOqhi4iIBIASuoiISABoyF0kxvTcexEpCQnZQ9d96CIiItklZELXfegiIiLZJWRCFxERkeyU0EVERAJACV1ERCQAlNBFREQCQAldREQkAJTQRUREAkAJXUREJACU0EVERAJAj36VhKbKdCIinoTsoevRryIiItklZELXo19FRESyS8iELiIiItkpoYuIiASAErqIiEgAKKGLiIgEgBK6iIhIACihi4iIBIASuoiISAAooYuIiASAErqIiEgAKKGLiIgEQEImdD3LXUREJLuETOh6lruIiEh2CZnQRUREJDvVQy8hDV9tWOA8K29eWQKRiIhIEKmHLiIiEgBK6CIiIgGghC4iIhIAiZ3Q09Nh7Fjv54MHIRSCceO8dmam15440WtnZHjtSZO89vbtXnv6dK+9ZQsT3hxK23XLAKjx0zYmvDmUVuvTADh3l/c68+ZFbHsvbDjktbce9trfH/bX57fTvOXrr8vklSfXUXvjfgAafeW1kzcfAKDZmr1ePOvWAVBl717O++5bTjh4EICT9u7hvO++pfwhf3vTp3vzb9/utSdN8tpZt/JNnOi1MzO99rhxXttfH2PHeu0so0fDlVcebb/4InTqdLT9zDPQtWu4eesnk/jH5L+E24P+9zbPTf2/cPvO/46HPn2OLj9nP0zdd7Q9az9Mj2jfey8MHny0fffd3r8sgwd782QZOBDuvz/crLF5M9W3bwu3z960iepZ+wagVy8YPvxou3t3GDHiaLtrV+89ZunUydsHWa680ttHWUKhqI+9kw/sZcKbQ+mQvgiAUzMzmPDmUNp//QkAZ+z50Zt/5kxv+Ywj3rGzzv9d/+i31/vt9HRv/kXe+li1ymsvWeK109K8tn/ssWQJhEKceMA71irvy+S8776l4s9eu0qm167w88/e/LNmcd2031F1z1YAzvtuMddN+x1VMncCkLx+EddN+13Mjr0+n/6bsW89Em73XzqV0e8+dnT+ESO831+WhQfgncyj7XkHYFLEsfXwwzz+r43h5m/f3sIjY74Pt38/YTMPvrbp6PyFPPbo3x8efvhou08fePzxo+0YHnsnHD7EhDeHct3qOQBUOrifCW8OpfMX8715sz73vvD3faZ/LKX77T1++2v/2NqwgVeeXMclq/cAUHPrz7zy5Drv8wlI3nyAV55cV+hjj1WrvPaiRV47Pd1rz5uX7XOPWbO89oYNXnvmTK+9ZYvXjvPnXs5j7zfvbeOvL24It2+bupUn/3m0PXjSD97xkeX++73jJ0tRPvfykdgJXURERAAw51y8YyiyZs2auaVLlxbb+pKH/rvAedYPv/ZoY1gU98EP8745FuYq9y/qXlzgvBev+aLgbcdQofZVIfZTYWlfRa8w++qF22cXOO/gl9oVKY78xGpfBfEuE+2r+IvHvjKzZc65Zrm9ph66iIhIACihi4iIBIASuoiISAAooYuIiASAErqIiEgAKKGLiIgEQEImdNVDFxERyS4hq60556YD05s1a3ZrvGMRiSfdMywiWRKyhy4iIiLZJWQPXYJNvU4RkcJTD11ERCQAlNBFREQCQAldREQkAJTQRUREAkAJXUREJACU0EVERAJACV1ERCQAlNBFREQCQAldREQkAJTQRUREAkAJXUREJACU0EVERAJACV1ERCQAlNBFREQCQAldREQkAJTQRUREAkAJXUREJACU0EVERAJACV1ERCQAEjKhm1kXMxuVkZER71BERERKhYRM6M656c65gUlJSfEORUREpFRIyIQuIiIi2Smhi4iIBIASuoiISAAooYuIiASAErqIiEgAKKGL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", "text/plain": [ "
" ] @@ -220,8 +288,8 @@ "labels = []\n", "for i, (meth, speeds) in enumerate(speed_methods.items()):\n", " labels = speeds.keys() if len(speeds.keys()) > len(labels) else labels\n", - " x = np.arange(len(speeds)) + 0.25*i - 0.25\n", - " plt.bar(x, speeds.values(), width=0.2, log=True, label=meth)\n", + " x = np.arange(len(speeds)) + 0.15*i - 0.3\n", + " plt.bar(x, speeds.values(), width=0.12, log=True, label=meth)\n", "\n", "plt.axhline(34.56, color='black', linestyle=':', label='10 ns/day')\n", "plt.axhline(3.456, color='red', linestyle=':', label='100 ns/day')\n", diff --git a/tests/test_neigbors.py b/tests/test_neigbors.py new file mode 100644 index 000000000..7ffd79f3b --- /dev/null +++ b/tests/test_neigbors.py @@ -0,0 +1,29 @@ +import pytest +from pytest import mark +from sklearn import neighbors +import torch as pt + +from torchmdnet.models.utils import DistanceBruteForce, Distance + +@mark.parametrize('num_atoms', [5, 7, 11, 13, 17]) +@mark.parametrize('device', ['cpu', 'cuda']) +def test_neighbors(num_atoms, device): + + if not pt.cuda.is_available() and device == 'cuda': + pytest.skip('No GPU') + + device = pt.device(device) + + # Generate random inputs + pos = (10 * pt.rand(num_atoms, 3, dtype=pt.float32, device=device) - 5) + + simple = Distance(0.0, 100.0) + brute_force = DistanceBruteForce() + + _, simple_distances, _ = simple(pos, None) + _, brute_force_distances, _ = brute_force(pos, None) + + simple_distances = simple_distances.sort().values + brute_force_distances = brute_force_distances.sort().values + + assert pt.allclose(simple_distances, brute_force_distances) \ No newline at end of file diff --git a/torchmdnet/models/model.py b/torchmdnet/models/model.py index c34840771..c4177792f 100644 --- a/torchmdnet/models/model.py +++ b/torchmdnet/models/model.py @@ -8,6 +8,7 @@ from torchmdnet.models import output_modules from torchmdnet.models.wrappers import AtomFilter from torchmdnet import priors +import warnings def create_model(args, prior_model=None, mean=None, std=None): @@ -25,6 +26,9 @@ def create_model(args, prior_model=None, mean=None, std=None): max_num_neighbors=args["max_num_neighbors"], ) + if "neighbors" in args: + shared_args["neighbors"] = args["neighbors"] + # representation network if args["model"] == "graph-network": from torchmdnet.models.torchmd_gn import TorchMD_GN @@ -100,7 +104,8 @@ def load_model(filepath, args=None, device="cpu", **kwargs): args = ckpt["hyper_parameters"] for key, value in kwargs.items(): - assert key in args, "Unknown hyperparameter '{key}'." + if not key in args: + warnings.warn(f'Unknown hyperparameter: {key}={value}') args[key] = value model = create_model(args) @@ -173,7 +178,8 @@ def forward(self, z, pos, batch: Optional[torch.Tensor] = None): x = self.prior_model(x, z, pos, batch) # aggregate atoms - out = scatter(x, batch, dim=0, reduce=self.reduce_op) + out = x.sum(0, keepdim=True) if self.reduce_op == "simple_add" else \ + scatter(x, batch, dim=0, reduce=self.reduce_op) # shift by data mean if self.mean is not None: diff --git a/torchmdnet/models/torchmd_gn.py b/torchmdnet/models/torchmd_gn.py index 468ecfee9..5342e1f42 100644 --- a/torchmdnet/models/torchmd_gn.py +++ b/torchmdnet/models/torchmd_gn.py @@ -4,6 +4,7 @@ NeighborEmbedding, CosineCutoff, Distance, + DistanceBruteForce, rbf_class_mapping, act_class_mapping, ) @@ -70,6 +71,7 @@ def __init__( max_z=100, max_num_neighbors=32, aggr="add", + neighbors="simple" ): super(TorchMD_GN, self).__init__() @@ -99,14 +101,19 @@ def __init__( self.cutoff_upper = cutoff_upper self.max_z = max_z self.aggr = aggr + self.neighbors = neighbors act_class = act_class_mapping[activation] self.embedding = nn.Embedding(self.max_z, hidden_channels) - self.distance = Distance( - cutoff_lower, cutoff_upper, max_num_neighbors=max_num_neighbors - ) + if self.neighbors == "simple": + self.distance = Distance(cutoff_lower, cutoff_upper, max_num_neighbors=max_num_neighbors) + elif self.neighbors == "brute_force": + self.distance = DistanceBruteForce() + else: + raise ValueError('neighbours') + self.distance_expansion = rbf_class_mapping[rbf_type]( cutoff_lower, cutoff_upper, num_rbf, trainable_rbf ) diff --git a/torchmdnet/models/utils.py b/torchmdnet/models/utils.py index f4a186916..6fc22edff 100644 --- a/torchmdnet/models/utils.py +++ b/torchmdnet/models/utils.py @@ -56,11 +56,11 @@ def reset_parameters(self): def forward(self, z, x, edge_index, edge_weight, edge_attr): # remove self loops - mask = edge_index[0] != edge_index[1] - if not mask.all(): - edge_index = edge_index[:, mask] - edge_weight = edge_weight[mask] - edge_attr = edge_attr[mask] + # mask = edge_index[0] != edge_index[1] + # if not mask.all(): + # edge_index = edge_index[:, mask] + # edge_weight = edge_weight[mask] + # edge_attr = edge_attr[mask] C = self.cutoff(edge_weight) W = self.distance_proj(edge_attr) * C.view(-1, 1) @@ -238,6 +238,26 @@ def forward(self, pos, batch): return edge_index, edge_weight, None +class DistanceBruteForce(nn.Module): + def __init__(self): + super().__init__() + + def forward(self, pos, batch): + + num_nodes = len(pos) + indices = torch.arange(0, num_nodes * (num_nodes - 1), device=pos.device) + + row = torch.div(indices, num_nodes - 1, rounding_mode='floor') + column = torch.div(indices, num_nodes, rounding_mode='floor') + column = torch.remainder(indices + column + 1, num_nodes) + + edge_index = torch.vstack((row, column)) + edge_vec = torch.index_select(pos, 0, row) - torch.index_select(pos, 0, column) + edge_weight = torch.norm(edge_vec, dim=-1) + + return edge_index, edge_weight, None + + class GatedEquivariantBlock(nn.Module): """Gated Equivariant Block as defined in Schütt et al. (2021): Equivariant message passing for the prediction of tensorial properties and molecular spectra