diff --git a/apps/HelloWasm/.gitignore b/apps/HelloWasm/.gitignore
index d5a65bf30984..5a54ff823fc8 100644
--- a/apps/HelloWasm/.gitignore
+++ b/apps/HelloWasm/.gitignore
@@ -1 +1,3 @@
 js/*
+build/
+bin/
diff --git a/apps/HelloWasm/Makefile b/apps/HelloWasm/Makefile
index b09473f74c1b..4de5b2ea6c43 100644
--- a/apps/HelloWasm/Makefile
+++ b/apps/HelloWasm/Makefile
@@ -75,7 +75,7 @@ clean:
 
 $(BIN)/$(HL_TARGET)/run: $(BIN)/$(HL_TARGET)/reaction_diffusion_init.a $(BIN)/$(HL_TARGET)/reaction_diffusion_update.a $(BIN)/$(HL_TARGET)/reaction_diffusion_render.a $(BIN)/$(HL_TARGET)/runtime.a
 	mkdir -p $(@D)
-	$(CXX) $(HALIDE_DISTRIB_PATH)/tools/RunGenMain.cpp $(BIN)/$(HL_TARGET)/reaction_diffusion_*.registration.cpp $^ -o $@ -I $(HALIDE_DISTRIB_PATH)/include $(IMAGE_IO_FLAGS) $(LDFLAGS)
+	$(CXX) $(CXXFLAGS) $(HALIDE_DISTRIB_PATH)/tools/RunGenMain.cpp $(BIN)/$(HL_TARGET)/reaction_diffusion_*.registration.cpp $^ -o $@ -I $(HALIDE_DISTRIB_PATH)/include $(IMAGE_IO_FLAGS) $(LDFLAGS)
 
 benchmark_native: $(BIN)/$(HL_TARGET)/run
 	$(BIN)/$(HL_TARGET)/run --benchmarks=all --benchmark_min_time=1 --name=reaction_diffusion_init --output_extents=[1024,1024] --parsable_output
diff --git a/apps/HelloWasm/README.md b/apps/HelloWasm/README.md
index caf0a6f8128e..b947bdeb0ff0 100644
--- a/apps/HelloWasm/README.md
+++ b/apps/HelloWasm/README.md
@@ -6,7 +6,7 @@ To try it out,
 
 - Build with `make all`
 
-- Run a local webserver using, e.g.: python3 -m http.server 8080 &
+- Run a local webserver: `python3 ../serve_with_cross_origin_isolation.py`. (This is a trivial wrapper about Python 3's `http.server` that also sets the necessary headers for "cross-origin isolated" that allow for threading to work in recent browsers; see https://developer.chrome.com/blog/enabling-shared-array-buffer/ for more information.)
 
 - Load Google chrome (at least version 84), go to chrome://flags, and turn on all the experimental webassembly stuff (e.g. threads, simd). If you don't do this, only the single-threaded scalar variant will work (at the time of writing).
 
diff --git a/apps/HelloWasm/serve_with_cross_origin_isolation.py b/apps/HelloWasm/serve_with_cross_origin_isolation.py
new file mode 100644
index 000000000000..6d557c39609f
--- /dev/null
+++ b/apps/HelloWasm/serve_with_cross_origin_isolation.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+import sys
+
+class MyRequestHandler(SimpleHTTPRequestHandler):
+    
+    def end_headers(self):
+        self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
+        self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
+        return super(MyRequestHandler, self).end_headers()
+
+    def do_OPTIONS(self):
+        self.send_response(200)
+        self.end_headers()
+
+host = sys.argv[1] if len(sys.argv) > 2 else '127.0.0.1'
+port = int(sys.argv[len(sys.argv)-1]) if len(sys.argv) > 1 else 8080
+
+print("Listening on {}:{}".format(host, port))
+httpd = HTTPServer((host, port), MyRequestHandler)
+httpd.serve_forever()
diff --git a/apps/hannk/interpreter/lower.cpp b/apps/hannk/interpreter/lower.cpp
index 062b5d73681d..6ec79c16bb7e 100644
--- a/apps/hannk/interpreter/lower.cpp
+++ b/apps/hannk/interpreter/lower.cpp
@@ -108,11 +108,17 @@ OpPtr lower_tflite_fullyconnected(const TensorPtr &input, const TensorPtr &filte
             assert(i.min == 0);
         }
 #endif
-        int c_extent = 1;
-        int b_extent = bounds.back().extent();
-        for (size_t i = 0; i + 1 < bounds.size(); i++) {
-            c_extent *= bounds[i].extent();
+        // It's important that we preserve the invariants:
+        //      input_reshaped[0] == filter[0]
+        //      input_reshaped[1] = input->number_of_elements()/input_reshaped[0]
+        int c_extent = filter->bounds()[0].extent();
+        int num_elems = 1;
+        for (size_t i = 0; i < bounds.size(); i++) {
+            num_elems *= bounds[i].extent();
         }
+        const int b_extent = num_elems / c_extent;
+        HCHECK(c_extent * b_extent == num_elems);
+
         Box reshaped_bounds = {{0, c_extent - 1}, {0, b_extent - 1}};
         TensorPtr input_reshaped =
             std::make_shared<Tensor>(input->name() + ".reshaped", input->type(), std::move(reshaped_bounds), input->quantization());
diff --git a/apps/hannk/interpreter/ops.cpp b/apps/hannk/interpreter/ops.cpp
index ff8dfa929fbc..4251f803e2b7 100644
--- a/apps/hannk/interpreter/ops.cpp
+++ b/apps/hannk/interpreter/ops.cpp
@@ -44,6 +44,10 @@ std::string dims_to_string(const halide_buffer_t *buf) {
     oss << "}";
     return oss.str();
 }
+
+std::string dims_to_string(const HalideBuffer<void> &buf) {
+    return dims_to_string(buf.raw_buffer());
+}
 #endif
 
 // Split a dimension d into two new dimensions. Dim d will have min 0
diff --git a/apps/hannk/interpreter/tensor.cpp b/apps/hannk/interpreter/tensor.cpp
index 40c6f234eea0..cf46d55a0249 100644
--- a/apps/hannk/interpreter/tensor.cpp
+++ b/apps/hannk/interpreter/tensor.cpp
@@ -74,17 +74,23 @@ TensorStoragePtr Tensor::storage() {
 void Tensor::set_external_buffer(HalideBuffer<void> external_buffer) {
     assert(!is_dynamic());
     assert(is_external());
-    assert(storage_ == nullptr);
 
     // No: it's ok to set this to different values over time,
     // so don't assert that host is currently null (or already equal to the new value)
     // assert(!is_allocated());
 
-    for (int i = 0; i < buffer_.dimensions(); i++) {
-        assert(external_buffer.dim(i).min() == buffer_.dim(i).min());
-        assert(external_buffer.dim(i).extent() == buffer_.dim(i).extent());
+    storage()->buffer = std::move(external_buffer);
+    finish_buffer_allocation();
+
+    if (aliases_ != nullptr) {
+        for (const auto &weak : *aliases_) {
+            TensorPtr tp = weak.lock();  // null if the weak_ptr has expired
+            if (tp != nullptr && tp.get() != this) {
+                assert(!tp->is_external());
+                tp->finish_buffer_allocation();
+            }
+        }
     }
-    buffer_ = std::move(external_buffer);
 }
 
 void Tensor::allocate_from_arena_pointer(void *host) {
@@ -117,7 +123,7 @@ void Tensor::finish_buffer_allocation() {
     halide_buffer_t *raw_storage_buffer = storage_buffer.raw_buffer();
     assert(raw_storage_buffer->host);
 
-    if (is_reshape_alias_) {
+    if (alias_type_ == AliasType::Reshaped) {
         assert(raw_storage_buffer->number_of_elements() == buffer_.number_of_elements());
         assert(raw_storage_buffer->type == buffer_.type());
         assert(storage_offset_.empty());
@@ -191,67 +197,162 @@ void Tensor::resize_dynamic(const Box &new_shape) {
     storage_ = nullptr;
 }
 
-void Tensor::set_alias_of(const TensorPtr &t, const SmallVector<int, max_rank> &storage_offset, bool is_reshape) {
-    assert(!is_dynamic());
-    assert(!is_external());
-    assert(!is_alias());
+bool Tensor::has_external_alias() const {
+    if (aliases_ != nullptr) {
+        for (const auto &weak : *aliases_) {
+            TensorPtr tp = weak.lock();  // null if the weak_ptr has expired
+            if (tp != nullptr && tp->is_external()) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+// Return true iff 'this' can be an alias of 'source' of the given type.
+bool Tensor::can_alias(const TensorPtr &source, AliasType alias_type) const {
+    if (alias_type == AliasType::None || this == source.get()) {
+        return false;  // Bad inputs, just say no
+    }
+
+    if (this->is_allocated()) {
+        // Can't alias a tensor that already has an allocation.
+        return false;
+    }
 
-    // No: 't' may (or may not) already have is_alias_ = true,
-    // but both will be considered an alias after this call.
-    // assert(!t->is_alias_);
+    // If either tensor is dynamic, can't alias them.
+    if (this->is_dynamic() || source->is_dynamic()) {
+        return false;
+    }
 
-    storage_ = t->storage();
-    storage_offset_ = storage_offset;
+    if (this->type().bytes() != source->type().bytes()) {
+        // We can't alias tensors with types of different size.
+        return false;
+    }
 
-    if (is_reshape) {
-        is_reshape_alias_ = true;
+    if (this->alias_type() != AliasType::None || this->aliases_ != nullptr) {
+        // Can't alias a tensor multiple times.
+        return false;
+    }
 
-        // No: assume that t->storage() matches the rank+dimensions expected by t
-        // (or, if not, that t->is_reshape_alias_ is already set to true).
-        //
-        // t->is_reshape_alias_ = true;
+    if (source->alias_type() != AliasType::None && source->alias_type() != alias_type) {
+        // The source of the aliasing must either be unaliased or of the type we want
+        return false;
     }
 
-#ifndef NDEBUG
-    if (is_reshape) {
-        assert(storage_offset_.empty());
-        assert(this->buffer().type() == t->buffer().type());
-        assert(this->buffer().number_of_elements() == t->buffer().number_of_elements());
-    } else {
-        // Reality-check.
-        Box offset_bounds = bounds();
-        for (int i = 0; i < (int)storage_offset_.size(); i++) {
-            offset_bounds[i] += storage_offset_[i];
+    if (alias_type == AliasType::Offset) {
+        // AliasType::Offset allows for NO external tensors in the alias group
+        if (this->is_external() || source->is_external()) {
+            return false;
         }
-        auto &shared_buffer = storage_->buffer;
-        assert(shared_buffer.type().bytes() == type().bytes());
-        assert(shared_buffer.dimensions() == (int)offset_bounds.size());
-        assert(!shared_buffer.data());
-
-        // Check that the storage is big enough for this buffer.
-        for (int i = 0; i < shared_buffer.dimensions(); i++) {
-            assert(offset_bounds[i].min >= shared_buffer.dim(i).min());
-            assert(offset_bounds[i].max <= shared_buffer.dim(i).max());
+
+        if (this->rank() != source->rank()) {
+            // AliasType::Offset can't alias tensors with different rank.
+            return false;
+        }
+    } else if (alias_type == AliasType::Reshaped) {
+        // AliasType::Reshaped allows for at most one external tensor in the alias group
+        const bool this_external = this->is_external() || this->has_external_alias();
+        const bool source_external = source->is_external() || source->has_external_alias();
+        if (this_external && source_external) {
+            return false;
         }
     }
+
+    return true;
+}
+
+/*static*/ void Tensor::make_offset_alias(TensorPtr alias, TensorPtr source, const TensorOffset &storage_offset) {
+    assert(alias->can_alias(source, AliasType::Offset));
+
+    if (source->aliases_ == nullptr) {
+        source->aliases_ = std::make_shared<std::vector<std::weak_ptr<Tensor>>>(1, source);
+        source->alias_type_ = AliasType::Offset;
+    }
+
+    assert(alias->aliases_ == nullptr);
+    alias->aliases_ = source->aliases_;
+    alias->aliases_->push_back(alias);
+
+    assert(alias->alias_type_ == AliasType::None);
+    alias->alias_type_ = AliasType::Offset;
+
+    assert(alias->storage_ == nullptr);
+    alias->storage_ = source->storage();
+    assert(alias->storage_offset_.empty());
+    alias->storage_offset_ = storage_offset;
+
+#ifndef NDEBUG
+    // Reality-check.
+    Box offset_bounds = alias->bounds();
+    for (int i = 0; i < (int)alias->storage_offset_.size(); i++) {
+        offset_bounds[i] += alias->storage_offset_[i];
+    }
+    auto &shared_buffer = alias->storage_->buffer;
+    assert(shared_buffer.type().bytes() == alias->type().bytes());
+    assert(shared_buffer.dimensions() == (int)offset_bounds.size());
+    assert(!shared_buffer.data());
+
+    // Check that the storage is big enough for this buffer.
+    for (int i = 0; i < shared_buffer.dimensions(); i++) {
+        assert(offset_bounds[i].min >= shared_buffer.dim(i).min());
+        assert(offset_bounds[i].max <= shared_buffer.dim(i).max());
+    }
 #endif
+}
+
+/*static*/ void Tensor::make_reshape_alias(TensorPtr alias, TensorPtr source) {
+    assert(alias->can_alias(source, AliasType::Reshaped));
+
+    if (alias->is_external()) {
+        assert(!source->has_external_alias());
+    } else if (source->is_external()) {
+        assert(!alias->has_external_alias());
+    }
+
+    if (source->aliases_ == nullptr) {
+        source->aliases_ = std::make_shared<std::vector<std::weak_ptr<Tensor>>>(1, source);
+        assert(source->alias_type_ == AliasType::None);
+        source->alias_type_ = AliasType::Reshaped;
+    } else {
+        assert(source->alias_type_ == AliasType::Reshaped);
+    }
 
-    is_alias_ = true;
-    t->is_alias_ = true;
+    assert(alias->aliases_ == nullptr);
+    alias->aliases_ = source->aliases_;
+    alias->aliases_->push_back(alias);
+
+    assert(alias->alias_type_ == AliasType::None);
+    alias->alias_type_ = AliasType::Reshaped;
+
+    assert(alias->storage_ == nullptr);
+    alias->storage_ = source->storage();
+    assert(alias->storage_offset_.empty());
+
+#ifndef NDEBUG
+    assert(alias->storage_offset_.empty());
+    assert(alias->buffer().type().bytes() == source->buffer().type().bytes());
+    assert(alias->buffer().number_of_elements() == source->buffer().number_of_elements());
+#endif
 }
 
 void Tensor::dump(std::ostream &os) const {
-    os << "  " << buffer_.type() << " x ";
+    os << "  \"" << name() << "\" this:@" << (const void *)this;
+
+    os << " " << buffer_.type() << " x ";
 
-    const auto *b = buffer_.raw_buffer();
-    os << '{';
-    for (int i = 0; i < b->dimensions; i++) {
-        if (i > 0) {
-            os << ", ";
+    const auto dump_dims = [&os](const halide_buffer_t *b) {
+        os << '{';
+        for (int i = 0; i < b->dimensions; i++) {
+            if (i > 0) {
+                os << ", ";
+            }
+            os << b->dim[i];
         }
-        os << b->dim[i];
-    }
-    os << '}';
+        os << '}';
+    };
+
+    dump_dims(buffer_.raw_buffer());
 
     if (is_allocated()) {
         os << " allocated";
@@ -267,9 +368,13 @@ void Tensor::dump(std::ostream &os) const {
     if (is_dynamic()) {
         os << " dynamic";
     }
-    if (is_alias()) {
-        os << " alias";
-        os << " storage_offset{";
+    if (alias_type_ != AliasType::None) {
+        os << (alias_type_ == AliasType::Offset ? " alias_offset{" : " alias_reshaped{");
+        for (const auto &weak : *aliases_) {
+            TensorPtr tp = weak.lock();  // null if the weak_ptr has expired
+            os << " " << (void *)tp.get();
+        }
+        os << " } storage_offset{";
         for (size_t i = 0; i < storage_offset_.size(); i++) {
             if (i > 0) {
                 os << ", ";
@@ -285,8 +390,12 @@ void Tensor::dump(std::ostream &os) const {
     }
 
     os << " storage:@" << (void *)storage_.get();
+    if (storage_) {
+        os << ' ';
+        dump_dims(storage_->buffer.raw_buffer());
+    }
 
-    os << " " << name() << " this:@" << (const void *)this << std::endl;
+    os << std::endl;
 }
 
 }  // namespace hannk
diff --git a/apps/hannk/interpreter/tensor.h b/apps/hannk/interpreter/tensor.h
index 19578102ba61..5d98269d372b 100644
--- a/apps/hannk/interpreter/tensor.h
+++ b/apps/hannk/interpreter/tensor.h
@@ -62,6 +62,14 @@ class TensorStorage {
 };
 using TensorStoragePtr = std::shared_ptr<TensorStorage>;
 
+enum class AliasType {
+    None,
+    Offset,    // The aliased Tensors are translated/cropped views of the same buffer.
+               // External tensors cannot be in an offset-alias group.
+    Reshaped,  // The aliased Tensors share the same host pointer and have identical elem_count but different shapes
+               // At most one External tensor can be in a reshape-alias group.
+};
+
 class Tensor {
     std::string name_;
     HalideBuffer<void> buffer_;
@@ -78,12 +86,13 @@ class Tensor {
     // for a Tensor to be dynamic if it is also constant or external.
     // Currently only used in conjunction with the TFLite delegate.
     bool is_dynamic_ = false;
-    // If true, this Tensor shares its TensorStorage with at least one other Tensor.
-    bool is_alias_ = false;
-    // If true, this Tensor is aliased to another via reshape (rather than cropping),
-    // and may have a different rank from the underlying storage.
-    // Only used if is_alias_ = true.
-    bool is_reshape_alias_ = false;
+
+    // All the Tensors in this list share their TensorStorage with each other.
+    // An arbitrary number of Tensors can alias each other, but at most *one* may be external.
+    //
+    // Note that this list should never have size() of 0 or 1 (the pointer should simply be null).
+    std::shared_ptr<std::vector<std::weak_ptr<Tensor>>> aliases_;
+    AliasType alias_type_ = AliasType::None;
 
     // Possibly shared storage for this tensor.
     TensorStoragePtr storage_;
@@ -98,6 +107,8 @@ class Tensor {
 
     void finish_buffer_allocation();
 
+    bool has_external_alias() const;
+
 public:
     Tensor() = delete;
     Tensor(std::string name, HalideBuffer<void> buffer, QuantizationInfo quantization = QuantizationInfo());
@@ -167,6 +178,7 @@ class Tensor {
 
     void set_external(bool external = true) {
         assert(!(external && is_dynamic()));
+        assert(!(external && alias_type() != AliasType::None));
         is_external_ = external;
     }
 
@@ -207,10 +219,17 @@ class Tensor {
 
     void resize_dynamic(const Box &new_shape);
 
-    bool is_alias() const {
-        return is_alias_;
+    // Return true iff this Tensor aliases another Tensor.
+    AliasType alias_type() const {
+        return alias_type_;
     }
-    void set_alias_of(const TensorPtr &t, const TensorOffset &offset = {}, bool is_reshape = false);
+
+    // Return true iff 'this' can be an alias of 'source' of the given type.
+    bool can_alias(const TensorPtr &source, AliasType alias_type) const;
+
+    // Needs to be static so we can pass via shared_ptr (necessary for weak_ptr usage internally)
+    static void make_offset_alias(TensorPtr alias, TensorPtr source, const TensorOffset &offset);
+    static void make_reshape_alias(TensorPtr alias, TensorPtr source);
 
     bool is_dense() const;
 
diff --git a/apps/hannk/interpreter/transforms.cpp b/apps/hannk/interpreter/transforms.cpp
index 554ace00fb22..cc1a9fbf147e 100644
--- a/apps/hannk/interpreter/transforms.cpp
+++ b/apps/hannk/interpreter/transforms.cpp
@@ -63,45 +63,35 @@ void remove_dead_ops(OpGroup *root) {
 
 namespace {
 
-// Check if a tensor already has storage configured.
-bool has_storage(const TensorPtr &t) {
-    return t->is_alias() || t->is_allocated();
-}
-
-// Try to alias outputs to inputs when it is safe.
-class InPlace : public LeafOpVisitor {
+class InPlaceReshape : public LeafOpVisitor {
     using OpVisitor::visit;
 
-    bool is_alias_possible(TensorPtr input, TensorPtr output) const {
-        // If either tensor is dynamic, can't alias them.
-        if (input->is_dynamic() || output->is_dynamic()) {
-            return false;
-        }
-
-        // If either tensor is external, can't alias them.
-        // TODO: maybe we can, but it's not clear how to update storage_.host in that case?
-        if (input->is_external() || output->is_external()) {
-            return false;
-        }
+    void visit(ReshapeOp *op) override {
+        TensorPtr input = op->input();
+        TensorPtr output = op->output();
 
-        if (input->type().bytes() != output->type().bytes()) {
-            // We can't alias tensors with types of different size.
-            return false;
-        }
+        HCHECK(input->type().bytes() == output->type().bytes());
+        HCHECK(input->is_dense() == output->is_dense());
+        HCHECK(input->number_of_elements() == output->number_of_elements());
 
-        // We can't alias an input that is an input or output of the root graph.
-        // TODO: We could, if we don't change the shape.
-        if (is_root_input_or_output(input)) {
-            return false;
+        if (input->can_alias(output, AliasType::Reshaped)) {
+            Tensor::make_reshape_alias(input, output);
+        } else if (output->can_alias(input, AliasType::Reshaped)) {
+            Tensor::make_reshape_alias(output, input);
         }
-
-        return true;
     }
+};
+
+// Try to alias outputs to inputs when it is safe.
+class InPlace : public LeafOpVisitor {
+    using OpVisitor::visit;
 
     // We can alias two tensors if the input is not used after the output is written,
     // and we meet a number of other requirements.
     bool maybe_alias_tensors(TensorPtr input, TensorPtr output, TensorOffset offset = {}) const {
-        if (!is_alias_possible(input, output)) {
+        // We can't alias an input that is an input or output of the root graph.
+        // TODO: We could, if we don't change the shape.
+        if (is_root_input_or_output(input)) {
             return false;
         }
 
@@ -111,11 +101,6 @@ class InPlace : public LeafOpVisitor {
             return false;
         }
 
-        // rank has to match for most aliasing (note that Reshape is an exception to this)
-        if (input->rank() != output->rank()) {
-            return false;
-        }
-
         // We can't grow the bounds of the tensor we alias with.
         // TODO: We could, if we allowed non-zero mins. We also
         // could allow the max to grow, just not the min.
@@ -125,16 +110,15 @@ class InPlace : public LeafOpVisitor {
             input_bounds_with_offset[i] += offset[i];
             output_bounds_with_negative_offset[i] -= offset[i];
         }
-        bool input_subset_of_output = is_subset_of(input_bounds_with_offset, output->bounds());
-        bool output_subset_of_input = is_subset_of(output_bounds_with_negative_offset, input->bounds());
-        if (input_subset_of_output && !has_storage(input)) {
-            input->set_alias_of(output, offset);
+
+        if (is_subset_of(input_bounds_with_offset, output->bounds()) && input->can_alias(output, AliasType::Offset)) {
+            Tensor::make_offset_alias(input, output, offset);
             return true;
-        } else if (output_subset_of_input && !has_storage(output)) {
+        } else if (is_subset_of(output_bounds_with_negative_offset, input->bounds()) && output->can_alias(input, AliasType::Offset)) {
             for (int &i : offset) {
                 i = -i;
             }
-            output->set_alias_of(input, offset);
+            Tensor::make_offset_alias(output, input, offset);
             return true;
         }
 
@@ -208,30 +192,6 @@ class InPlace : public LeafOpVisitor {
         maybe_alias_tensors(op->input(), op->output(), offset);
     }
 
-    void visit(ReshapeOp *op) override {
-        const TensorPtr &input = op->input();
-        const TensorPtr &output = op->output();
-
-        // Reshape is unusual in that it's OK to alias Reshapes with mismatched rank
-        // (indeed, this is almost always the case), so we handle everything here
-        // instead of calling maybe_alias_tensors().
-        if (!is_alias_possible(input, output)) {
-            return;
-        }
-
-        if (!input->is_dense() || !output->is_dense()) {
-            // Can't alias a Reshape unless both Tensors have dense strides.
-            return;
-        }
-
-        constexpr bool is_reshape = true;
-        if (!has_storage(input)) {
-            input->set_alias_of(output, {}, is_reshape);
-        } else if (!has_storage(output)) {
-            output->set_alias_of(input, {}, is_reshape);
-        }
-    }
-
     const Op *const root_;
 
     bool is_root_input_or_output(const TensorPtr &t) const {
@@ -247,6 +207,12 @@ class InPlace : public LeafOpVisitor {
 }  // namespace
 
 void in_place(Op *op) {
+    // Always check for ReshapeOp before anything else; we want
+    // to try our best to alias those tensors, and the luck of the
+    // draw could put other aliases in place first that could thwart this.
+    InPlaceReshape handle_reshapes;
+    op->accept(&handle_reshapes);
+
     InPlace v(op);
     op->accept(&v);
 }
diff --git a/apps/hannk/test/misc/bad_fully_connected.tflite b/apps/hannk/test/misc/bad_fully_connected.tflite
new file mode 100644
index 000000000000..ba0c09b7d119
Binary files /dev/null and b/apps/hannk/test/misc/bad_fully_connected.tflite differ
