239 lines
8.1 KiB
Python
239 lines
8.1 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
# pylint: disable=invalid-name
|
|
# ruff: noqa: E731
|
|
"""General LSTM implementation using TE scan."""
|
|
|
|
from tvm import te, tirx
|
|
from tvm.topi import tag
|
|
|
|
|
|
def lstm(
|
|
Xs,
|
|
Wi,
|
|
Wh,
|
|
Bi=None,
|
|
Bh=None,
|
|
h_init=None,
|
|
c_init=None,
|
|
proj=None,
|
|
p_i=None,
|
|
p_f=None,
|
|
p_o=None,
|
|
f_act=tirx.sigmoid,
|
|
g_act=tirx.tanh,
|
|
h_act=tirx.tanh,
|
|
reverse=False,
|
|
weight_layout: str = "IFGO",
|
|
):
|
|
"""General LSTM implemented using TE scan.
|
|
|
|
Parameters
|
|
----------
|
|
Xs : te.Tensor
|
|
Input sequence with shape `(seq_len, batch_size, in_dim)`
|
|
Wi : te.Tensor
|
|
Input weight matrix with shape `(4 * hidden_dim, in_dim)`. The weights are packed according
|
|
to `weight_layout`.
|
|
Wh : te.Tensor
|
|
Hidden weight matrix with shape `(4 * hidden_dim, hidden_dim or proj_dim)`. Packed as `Wh`.
|
|
Bi : te.Tensor, optional
|
|
Input bias with shape `(4 * hidden_dim,)`, by default None. Packed as `Wh`.
|
|
Bh : te.Tensor, optional
|
|
Hidden bias with shape as `Bi`, by default None. Packed as `Wh`.
|
|
h_init : te.Tensor, optional
|
|
Initial hidden state with shape `(batch_size, hidden_dim or proj_dim)`, zero if None
|
|
c_init : te.Tensor, optional
|
|
Initial cell state with same shape as `h_init`, zero if None
|
|
proj : te.Tensor, optional
|
|
Projection matrix with shape `(proj_dim, hidden_dim)`, by default None
|
|
p_i, p_f, p_o : te.Tensor, optional
|
|
Peephole LSTM matrices with shape `(batch_size, hidden_dim)`, by default None
|
|
f_act, g_act, h_act : F, optional
|
|
Gate activation functions
|
|
reverse : bool, optional
|
|
Whether to process `Xs` in reverse, by default False
|
|
weight_layout : str, optional
|
|
The packed weight layout for gates, by default "IFGO". Note: I = input, F = forget,
|
|
G = cell, O = output.
|
|
|
|
Returns
|
|
-------
|
|
result : te.Tensor, te.Tensor
|
|
Tuple of hidden states (with shape `(seq_len, batch_size, hidden_dim or proj_dim)`), and
|
|
cell states (with shape `(seq_len, batch_size, hidden_dim)`).
|
|
"""
|
|
assert len(weight_layout) == 4 and sorted(weight_layout) == sorted("IFGO"), (
|
|
f'given weight layout "{weight_layout}" is not a permutation of "IFGO"'
|
|
)
|
|
|
|
i_gate_idx = weight_layout.find("I")
|
|
f_gate_idx = weight_layout.find("F")
|
|
g_gate_idx = weight_layout.find("G")
|
|
o_gate_idx = weight_layout.find("O")
|
|
|
|
seq_len, batch_size, in_dim = Xs.shape
|
|
assert Wi.shape[0] % 4 == 0, (
|
|
f"dim 0 of input weight should be 4 * hidden_dim, but {Wi.shape[0]} is not divisible by 4"
|
|
)
|
|
hidden_dim = Wi.shape[0] // 4
|
|
proj_dim = hidden_dim
|
|
if proj is not None:
|
|
proj_dim = proj.shape[0]
|
|
|
|
# te.scan uses up 1 element for the initial value
|
|
scan_len = seq_len + 1
|
|
|
|
# precompute input-hidden matmul outside the scan
|
|
ki = te.reduce_axis((0, in_dim), name="ki2h")
|
|
Xi2h = te.compute(
|
|
(seq_len * batch_size, 4 * hidden_dim),
|
|
lambda tb, ij: te.sum(Xs[(tb // batch_size), tb % batch_size, ki] * Wi[ij, ki], axis=ki),
|
|
name="Xi2h",
|
|
)
|
|
if Bi is not None:
|
|
Xi2h = te.compute(
|
|
Xi2h.shape, lambda tb, ij: Xi2h[tb, ij] + Bi[ij], name="Xi2h_bias", tag=tag.INJECTIVE
|
|
)
|
|
|
|
h_state = te.placeholder((scan_len, batch_size, proj_dim), name="h_state")
|
|
c_state = te.placeholder((scan_len, batch_size, hidden_dim), name="c_state")
|
|
h_init = te.compute(
|
|
(1, batch_size, proj_dim),
|
|
lambda _, b, i: h_init[b, i] if h_init is not None else 0.0,
|
|
name="h_init",
|
|
)
|
|
c_init = te.compute(
|
|
(1, batch_size, hidden_dim),
|
|
lambda _, b, i: c_init[b, i] if c_init is not None else 0.0,
|
|
name="c_init",
|
|
)
|
|
|
|
# begin scan computations, first the (batched) hidden-hidden dense
|
|
kh = te.reduce_axis((0, proj_dim), name="kh2h")
|
|
s_h2h = te.compute(
|
|
(scan_len, batch_size, 4, hidden_dim),
|
|
lambda t, b, i, j: te.sum(h_state[t - 1, b, kh] * Wh[i * hidden_dim + j, kh], axis=kh),
|
|
name="s_h2h",
|
|
)
|
|
if Bh is not None:
|
|
s_h2h = te.compute(
|
|
s_h2h.shape,
|
|
lambda t, b, i, j: s_h2h[t, b, i, j] + Bh[i * hidden_dim + j],
|
|
name="s_h2h_bias",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
|
|
# helper to reverse time if scanning backwards
|
|
get_x_t = lambda t: seq_len - t if reverse else t - 1
|
|
|
|
gates = te.compute(
|
|
(scan_len, batch_size, 4, hidden_dim),
|
|
lambda t, b, i, j: (
|
|
Xi2h[get_x_t(t) * batch_size + b, i * hidden_dim + j] + s_h2h[t, b, i, j]
|
|
),
|
|
name="gates",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
|
|
# helper to correctly read each gate dense from the batched output
|
|
read_gate = lambda t, b, j, idx: gates[t, b, idx, j]
|
|
|
|
gate_shape = (scan_len, batch_size, hidden_dim)
|
|
|
|
# compute the activated gates (and do some extra stuff if peephole weights are present)
|
|
if p_i is not None and p_f is not None:
|
|
i_gate = te.compute(
|
|
gate_shape,
|
|
lambda t, b, j: f_act(
|
|
read_gate(t, b, j, i_gate_idx) + p_i[b, j] * c_state[t - 1, b, j]
|
|
),
|
|
name="i_gate_p",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
f_gate = te.compute(
|
|
gate_shape,
|
|
lambda t, b, j: f_act(
|
|
read_gate(t, b, j, f_gate_idx) + p_f[b, j] * c_state[t - 1, b, j]
|
|
),
|
|
name="f_gate_p",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
else:
|
|
i_gate = te.compute(
|
|
gate_shape,
|
|
lambda *i: f_act(read_gate(*i, i_gate_idx)),
|
|
name="i_gate",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
f_gate = te.compute(
|
|
gate_shape,
|
|
lambda *i: f_act(read_gate(*i, f_gate_idx)),
|
|
name="f_gate",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
|
|
g_gate = te.compute(
|
|
gate_shape, lambda *i: g_act(read_gate(*i, g_gate_idx)), name="g_gate", tag=tag.INJECTIVE
|
|
)
|
|
|
|
next_c = te.compute(
|
|
gate_shape,
|
|
lambda t, b, j: f_gate[t, b, j] * c_state[t - 1, b, j] + i_gate[t, b, j] * g_gate[t, b, j],
|
|
name="next_c",
|
|
)
|
|
|
|
if p_o is not None:
|
|
o_gate = te.compute(
|
|
gate_shape,
|
|
lambda t, b, j: f_act(read_gate(t, b, j, o_gate_idx) + p_o[b, j] * next_c[t, b, j]),
|
|
name="o_gate_p",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
else:
|
|
o_gate = te.compute(
|
|
gate_shape,
|
|
lambda *i: f_act(read_gate(*i, o_gate_idx)),
|
|
name="o_gate",
|
|
tag=tag.INJECTIVE,
|
|
)
|
|
|
|
next_h = te.compute(gate_shape, lambda *i: o_gate(*i) * h_act(next_c(*i)), name="next_h")
|
|
|
|
# project hidden state back to proj_dim if projection matrix is present
|
|
if proj is not None:
|
|
kr = te.reduce_axis((0, hidden_dim), name="kh2p")
|
|
next_h = te.compute(
|
|
(scan_len, batch_size, proj_dim),
|
|
lambda t, b, j: te.sum(next_h[t, b, kr] * proj[j, kr], axis=kr),
|
|
name="next_h_proj",
|
|
)
|
|
|
|
scan_h, scan_c = te.scan(
|
|
[h_init, c_init], [next_h, next_c], [h_state, c_state], name="lstm_scan"
|
|
)
|
|
|
|
# drop the initial values, TODO(@altanh): is there a better way?
|
|
scan_h = te.compute(
|
|
(seq_len, batch_size, proj_dim), lambda t, b, j: scan_h[t + 1, b, j], name="hidden_states"
|
|
)
|
|
scan_c = te.compute(
|
|
(seq_len, batch_size, hidden_dim), lambda t, b, j: scan_c[t + 1, b, j], name="cell_states"
|
|
)
|
|
|
|
return scan_h, scan_c
|