diff --git a/Projects/AnalysisBase/package_filters.txt b/Projects/AnalysisBase/package_filters.txt
index d9631a2366a70fb67da56a99174899ae0ee073cf..aa36f316318aae75c688c2722718b2b966ece4c7 100644
--- a/Projects/AnalysisBase/package_filters.txt
+++ b/Projects/AnalysisBase/package_filters.txt
@@ -105,6 +105,7 @@
 + Reconstruction/egamma/egammaUtils
 + Reconstruction/tauRecTools
 + Reconstruction/PanTau/PanTauAlgs
++ Reconstruction/LwtnnUtils
 + Tools/ART
 + Tools/DirectIOART
 + Tools/PathResolver
diff --git a/Reconstruction/LwtnnUtils/CMakeLists.txt b/Reconstruction/LwtnnUtils/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a10e47c1630b2f0d1d22cde527f3896610ed0d0d
--- /dev/null
+++ b/Reconstruction/LwtnnUtils/CMakeLists.txt
@@ -0,0 +1,19 @@
+# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+#
+
+# Declare the package name:
+atlas_subdir( LwtnnUtils )
+
+# External dependencies:
+find_package( lwtnn )
+find_package( Eigen )
+
+# Build a shared library:
+atlas_add_library( LwtnnUtils
+  LwtnnUtils/*.h                # <-- for Xcode, not needed otherwise
+  src/FastGraph.cxx
+  src/FastInputPreprocessor.cxx
+  PUBLIC_HEADERS LwtnnUtils
+  INCLUDE_DIRS ${LWTNN_INCLUDE_DIRS} ${EIGEN_INCLUDE_DIRS}
+  LINK_LIBRARIES ${LWTNN_LIBRARIES} ${EIGEN_LIBRARIES} )
+
diff --git a/Reconstruction/LwtnnUtils/LwtnnUtils/FastGraph.h b/Reconstruction/LwtnnUtils/LwtnnUtils/FastGraph.h
new file mode 100644
index 0000000000000000000000000000000000000000..89e26d44d7fe31a2bda6c99504904ce35944874d
--- /dev/null
+++ b/Reconstruction/LwtnnUtils/LwtnnUtils/FastGraph.h
@@ -0,0 +1,72 @@
+// this is -*- C++ -*-
+/*
+  Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+*/
+
+// Any modifications to this file may be copied to lwtnn[1] without
+// attribution.
+//
+// [1]: https::www.github.com/lwtnn/lwtnn
+
+#ifndef LWTNN_UTILS_FAST_GRAPH_H
+#define LWTNN_UTILS_FAST_GRAPH_H
+
+#include "lwtnn/lightweight_network_config.hh"
+
+#include <Eigen/Dense>
+
+namespace lwt {
+  class Graph;
+}
+
+namespace lwt::atlas {
+
+  class FastInputPreprocessor;
+  class FastInputVectorPreprocessor;
+  class InputOrder;
+
+  struct SourceIndices
+  {
+    std::vector<size_t> scalar;
+    std::vector<size_t> sequence;
+  };
+
+  // Graph class
+  class FastGraph
+  {
+  public:
+    // Since a graph has multiple input nodes, we actually call
+    typedef std::vector<Eigen::VectorXd> NodeVec;
+    typedef std::vector<Eigen::MatrixXd> SeqNodeVec;
+
+
+    // In cases where the graph has multiple outputs, we have to
+    // define a "default" output, so that calling "compute" with no
+    // output specified doesn't lead to ambiguity.
+    FastGraph(const GraphConfig& config, const InputOrder& order,
+              std::string default_output = "");
+
+    ~FastGraph();
+    FastGraph(FastGraph&) = delete;
+    FastGraph& operator=(FastGraph&) = delete;
+
+    // The simpler "compute" function
+    Eigen::VectorXd compute(const NodeVec&, const SeqNodeVec& = {}) const;
+
+  private:
+    typedef FastInputPreprocessor IP;
+    typedef FastInputVectorPreprocessor IVP;
+    typedef std::vector<IP*> Preprocs;
+    typedef std::vector<IVP*> VecPreprocs;
+
+    Eigen::VectorXd compute(const NodeVec&, const SeqNodeVec&, size_t) const;
+    Graph* m_graph;
+    Preprocs m_preprocs;
+    VecPreprocs m_vec_preprocs;
+    size_t m_default_output;
+    // the mapping from a node in the network to a user input node
+    SourceIndices m_input_indices;
+  };
+}
+
+#endif
diff --git a/Reconstruction/LwtnnUtils/LwtnnUtils/FastInputPreprocessor.h b/Reconstruction/LwtnnUtils/LwtnnUtils/FastInputPreprocessor.h
new file mode 100644
index 0000000000000000000000000000000000000000..b04688760d61a3276a67762fbbdc7e96e8494007
--- /dev/null
+++ b/Reconstruction/LwtnnUtils/LwtnnUtils/FastInputPreprocessor.h
@@ -0,0 +1,57 @@
+// this is -*- C++ -*-
+/*
+  Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+*/
+
+// Any modifications to this file may be copied to lwtnn[1] without
+// attribution.
+//
+// [1]: https::www.github.com/lwtnn/lwtnn
+
+
+#ifndef LWTNN_UTILS_FAST_INPUT_PREPROCESSOR_H
+#define LWTNN_UTILS_FAST_INPUT_PREPROCESSOR_H
+
+#include "lwtnn/lightweight_network_config.hh"
+#include "lwtnn/Exceptions.hh"
+
+#include <Eigen/Dense>
+#include <vector>
+
+namespace lwt::atlas {
+
+  using Eigen::VectorXd;
+  using Eigen::MatrixXd;
+
+  // ______________________________________________________________________
+  // input preprocessor (handles normalization and packing into Eigen)
+
+  class FastInputPreprocessor
+  {
+  public:
+    FastInputPreprocessor(const std::vector<Input>& inputs,
+                          const std::vector<std::string>& order);
+    VectorXd operator()(const VectorXd&) const;
+  private:
+    // input transformations
+    VectorXd m_offsets;
+    VectorXd m_scales;
+    std::vector<size_t> m_indices;
+  };
+
+  class FastInputVectorPreprocessor
+  {
+  public:
+    FastInputVectorPreprocessor(const std::vector<Input>& inputs,
+                                const std::vector<std::string>& order);
+    MatrixXd operator()(const MatrixXd&) const;
+  private:
+    // input transformations
+    VectorXd m_offsets;
+    VectorXd m_scales;
+    std::vector<size_t> m_indices;
+  };
+}
+
+
+#endif
diff --git a/Reconstruction/LwtnnUtils/LwtnnUtils/InputOrder.h b/Reconstruction/LwtnnUtils/LwtnnUtils/InputOrder.h
new file mode 100644
index 0000000000000000000000000000000000000000..9df4677d991c4bad004aa173bb57c01c357acf40
--- /dev/null
+++ b/Reconstruction/LwtnnUtils/LwtnnUtils/InputOrder.h
@@ -0,0 +1,36 @@
+// this is -*- C++ -*-
+/*
+  Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+*/
+
+// Any modifications to this file may be copied to lwtnn[1] without
+// attribution.
+//
+// [1]: https::www.github.com/lwtnn/lwtnn
+
+#ifndef LWTNN_UTILS_INPUT_ORDER_H
+#define LWTNN_UTILS_INPUT_ORDER_H
+
+#include <vector>
+#include <string>
+
+namespace lwt::atlas {
+
+  // the user should specify what inputs they are going to feed to
+  // the network. This is different from the ordering that the
+  // network uses internally: some variables that are passed in
+  // might be dropped or reorganized.
+  typedef std::vector<
+    std::pair<std::string, std::vector<std::string>>
+    > order_t;
+
+  struct InputOrder
+  {
+    order_t scalar;
+    order_t sequence;
+  };
+
+}
+
+
+#endif
diff --git a/Reconstruction/LwtnnUtils/README.md b/Reconstruction/LwtnnUtils/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b585f9569edd5705e9dc494073e73cb13fb6bb06
--- /dev/null
+++ b/Reconstruction/LwtnnUtils/README.md
@@ -0,0 +1,17 @@
+lwtnn Utilities
+===============
+
+This is some Atlas-side helpers for the [lwtnn][1] package.
+
+At some point things here might be migrated upstream, but we keep this
+package around for development that might not want to wait for
+`AtlasExternals` to be updated.
+
+Package Contents
+----------------
+
+- `FastGraph`: Faster version of `lwt::LightweightGraph`. Takes
+  vectors rather than `std::map<std::string,...>` inputs.
+
+
+[1]: https://www.github.com/lwtnn/lwtnn
diff --git a/Reconstruction/LwtnnUtils/src/FastGraph.cxx b/Reconstruction/LwtnnUtils/src/FastGraph.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..b1cdef0b6112ad0c2b7fc99871a291b133452ecf
--- /dev/null
+++ b/Reconstruction/LwtnnUtils/src/FastGraph.cxx
@@ -0,0 +1,170 @@
+// this is -*- C++ -*-
+/*
+  Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+*/
+
+// Any modifications to this file may be copied to lwtnn[1] without
+// attribution.
+//
+// [1]: https::www.github.com/lwtnn/lwtnn
+
+#include "LwtnnUtils/FastGraph.h"
+#include "LwtnnUtils/FastInputPreprocessor.h"
+#include "LwtnnUtils/InputOrder.h"
+#include "lwtnn/Graph.hh"
+#include <Eigen/Dense>
+
+namespace {
+  using namespace Eigen;
+  using namespace lwt;
+  using namespace lwt::atlas;
+
+  typedef atlas::FastGraph::NodeVec NodeVec;
+  typedef atlas::FastInputPreprocessor IP;
+  typedef std::vector<IP*> Preprocs;
+  typedef atlas::FastGraph::SeqNodeVec SeqNodeVec;
+  typedef atlas::FastInputVectorPreprocessor IVP;
+  typedef std::vector<IVP*> VecPreprocs;
+
+
+  // this is used internally to ensure that we only look up map inputs
+  // when the network asks for them.
+  class LazySource: public ISource
+  {
+  public:
+    LazySource(const NodeVec&, const SeqNodeVec&,
+               const Preprocs&, const VecPreprocs&,
+               const SourceIndices& input_indices);
+    virtual VectorXd at(size_t index) const override;
+    virtual MatrixXd matrix_at(size_t index) const override;
+  private:
+    const NodeVec& m_nodes;
+    const SeqNodeVec& m_seqs;
+    const Preprocs& m_preprocs;
+    const VecPreprocs& m_vec_preprocs;
+    const SourceIndices& m_input_indices;
+  };
+
+  LazySource::LazySource(const NodeVec& n, const SeqNodeVec& s,
+                         const Preprocs& p, const VecPreprocs& v,
+                         const SourceIndices& i):
+    m_nodes(n), m_seqs(s), m_preprocs(p), m_vec_preprocs(v),
+    m_input_indices(i)
+  {
+  }
+  VectorXd LazySource::at(size_t index) const
+  {
+    const auto& preproc = *m_preprocs.at(index);
+    size_t source_index = m_input_indices.scalar.at(index);
+    if (source_index >= m_nodes.size()) {
+      throw NNEvaluationException(
+        "The NN needs an input VectorXd at position "
+        + std::to_string(source_index) + " but only "
+        + std::to_string(m_nodes.size()) + " inputs were given");
+    }
+    return preproc(m_nodes.at(source_index));
+  }
+  MatrixXd LazySource::matrix_at(size_t index) const
+  {
+    const auto& preproc = *m_vec_preprocs.at(index);
+    size_t source_index = m_input_indices.sequence.at(index);
+    if (source_index >= m_nodes.size()) {
+      throw NNEvaluationException(
+        "The NN needs an input MatrixXd at position "
+        + std::to_string(source_index) + " but only "
+        + std::to_string(m_nodes.size()) + " inputs were given");
+    }
+    return preproc(m_seqs.at(source_index));
+  }
+
+  // utility functions
+  //
+  // Build a mapping from the inputs in the saved network to the
+  // inputs that the user is going to hand us.
+  std::vector<size_t> get_node_indices(
+    const order_t& order,
+    const std::vector<lwt::InputNodeConfig>& inputs)
+  {
+    std::map<std::string, size_t> order_indices;
+    for (size_t i = 0; i < order.size(); i++) {
+      order_indices[order.at(i).first] = i;
+    }
+    std::vector<size_t> node_indices;
+    for (const lwt::InputNodeConfig& input: inputs) {
+      if (!order_indices.count(input.name)) {
+        throw NNConfigurationException("Missing input " + input.name);
+      }
+      node_indices.push_back(order_indices.at(input.name));
+    }
+    return node_indices;
+  }
+
+
+}
+namespace lwt::atlas {
+  // ______________________________________________________________________
+  // Fast Graph
+
+  typedef FastGraph::NodeVec NodeVec;
+  FastGraph::FastGraph(const GraphConfig& config, const InputOrder& order,
+                       std::string default_output):
+    m_graph(new Graph(config.nodes, config.layers))
+  {
+
+    m_input_indices.scalar = get_node_indices(
+      order.scalar, config.inputs);
+
+    m_input_indices.sequence = get_node_indices(
+      order.sequence, config.input_sequences);
+
+    for (size_t i = 0; i < config.inputs.size(); i++) {
+      const lwt::InputNodeConfig& node = config.inputs.at(i);
+      size_t input_node = m_input_indices.scalar.at(i);
+      std::vector<std::string> varorder = order.scalar.at(input_node).second;
+      m_preprocs.emplace_back(
+        new FastInputPreprocessor(node.variables, varorder));
+    }
+    for (size_t i = 0; i < config.input_sequences.size(); i++) {
+      const lwt::InputNodeConfig& node = config.input_sequences.at(i);
+      size_t input_node = m_input_indices.sequence.at(i);
+      std::vector<std::string> varorder = order.sequence.at(input_node).second;
+      m_vec_preprocs.emplace_back(
+        new FastInputVectorPreprocessor(node.variables, varorder));
+    }
+    if (default_output.size() > 0) {
+      if (!config.outputs.count(default_output)) {
+        throw NNConfigurationException("no output node" + default_output);
+      }
+      m_default_output = config.outputs.at(default_output).node_index;
+    } else if (config.outputs.size() == 1) {
+      m_default_output = config.outputs.begin()->second.node_index;
+    } else {
+      throw NNConfigurationException("you must specify a default output");
+    }
+  }
+
+  FastGraph::~FastGraph() {
+    delete m_graph;
+    for (auto& preproc: m_preprocs) {
+      delete preproc;
+      preproc = 0;
+    }
+    for (auto& preproc: m_vec_preprocs) {
+      delete preproc;
+      preproc = 0;
+    }
+  }
+
+  VectorXd FastGraph::compute(const NodeVec& nodes,
+                              const SeqNodeVec& seq) const {
+    return compute(nodes, seq, m_default_output);
+  }
+  VectorXd FastGraph::compute(const NodeVec& nodes,
+                              const SeqNodeVec& seq,
+                              size_t idx) const {
+    LazySource source(nodes, seq, m_preprocs, m_vec_preprocs,
+                      m_input_indices);
+    return m_graph->compute(source, idx);
+  }
+
+}
diff --git a/Reconstruction/LwtnnUtils/src/FastInputPreprocessor.cxx b/Reconstruction/LwtnnUtils/src/FastInputPreprocessor.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..01da6604e5e7d3646819eb909b19fceb8e4b724f
--- /dev/null
+++ b/Reconstruction/LwtnnUtils/src/FastInputPreprocessor.cxx
@@ -0,0 +1,114 @@
+// this is -*- C++ -*-
+/*
+  Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+*/
+
+// Any modifications to this file may be copied to lwtnn[1] without
+// attribution.
+//
+// [1]: https::www.github.com/lwtnn/lwtnn
+
+#include "LwtnnUtils/FastInputPreprocessor.h"
+#include "lwtnn/Exceptions.hh"
+
+namespace {
+  // utility functions
+  //
+  // Build a mapping from the inputs in the saved network to the
+  // inputs that the user is going to hand us.
+  std::vector<size_t> get_value_indices(
+    const std::vector<std::string>& order,
+    const std::vector<lwt::Input>& inputs)
+  {
+    std::map<std::string, size_t> order_indices;
+    for (size_t i = 0; i < order.size(); i++) {
+      order_indices[order.at(i)] = i;
+    }
+    std::vector<size_t> value_indices;
+    for (const lwt::Input& input: inputs) {
+      if (!order_indices.count(input.name)) {
+        throw lwt::NNConfigurationException("Missing input " + input.name);
+      }
+      value_indices.push_back(order_indices.at(input.name));
+    }
+    return value_indices;
+  }
+
+}
+
+namespace lwt::atlas {
+  // ______________________________________________________________________
+  // FastInput preprocessors
+
+  // simple feed-forwared version
+  FastInputPreprocessor::FastInputPreprocessor(
+    const std::vector<Input>& inputs,
+    const std::vector<std::string>& order):
+    m_offsets(inputs.size()),
+    m_scales(inputs.size())
+  {
+    size_t in_num = 0;
+    for (const auto& input: inputs) {
+      m_offsets(in_num) = input.offset;
+      m_scales(in_num) = input.scale;
+      in_num++;
+    }
+    m_indices = get_value_indices(order, inputs);
+  }
+  VectorXd FastInputPreprocessor::operator()(const VectorXd& in) const {
+    VectorXd invec(m_indices.size());
+    size_t input_number = 0;
+    for (size_t index: m_indices) {
+      if (static_cast<int>(index) >= in.rows()) {
+        throw NNEvaluationException(
+          "index " + std::to_string(index) + " is out of range, scalar "
+          "input only has " + std::to_string(in.rows()) + " entries");
+      }
+      invec(input_number) = in(index);
+      input_number++;
+    }
+    return (invec + m_offsets).cwiseProduct(m_scales);
+  }
+
+
+  // Input vector preprocessor
+  FastInputVectorPreprocessor::FastInputVectorPreprocessor(
+    const std::vector<Input>& inputs,
+    const std::vector<std::string>& order):
+    m_offsets(inputs.size()),
+    m_scales(inputs.size())
+  {
+    size_t in_num = 0;
+    for (const auto& input: inputs) {
+      m_offsets(in_num) = input.offset;
+      m_scales(in_num) = input.scale;
+      in_num++;
+    }
+    // require at least one input at configuration, since we require
+    // at least one for evaluation
+    if (in_num == 0) {
+      throw NNConfigurationException("need at least one input");
+    }
+    m_indices = get_value_indices(order, inputs);
+  }
+  MatrixXd FastInputVectorPreprocessor::operator()(const MatrixXd& in) const {
+    using namespace Eigen;
+    size_t n_cols = in.cols();
+    MatrixXd inmat(m_indices.size(), n_cols);
+    size_t in_num = 0;
+    for (size_t index: m_indices) {
+      if (static_cast<int>(index) >= in.rows()) {
+        throw NNEvaluationException(
+          "index " + std::to_string(index) + " is out of range, sequence "
+          "input only has " + std::to_string(in.rows()) + " entries");
+      }
+      inmat.row(in_num) = in.row(index);
+      in_num++;
+    }
+    if (n_cols == 0) {
+        return MatrixXd(m_indices.size(), 0);
+    }
+    return m_scales.asDiagonal() * (inmat.colwise() + m_offsets);
+  }
+
+}