@@ -1207,7 +1207,9 @@ def _train_tts_encoder(
12071207 from i6_experiments .users .zeyer .recog import recog_training_exp as recog_training_func
12081208
12091209 if single_stream :
1210- assert tts_waveform and not pseudo_speech_enc , "single_stream: waveform TTS path only"
1210+ assert tts_waveform or (pseudo_speech_enc and pseudo_enc_start_layer >= 0 ), (
1211+ "single_stream: waveform TTS, or layer-split pseudo-enc"
1212+ )
12111213
12121214 vocab = "spm10k"
12131215 task = get_librispeech_task_raw_v2 (
@@ -1398,7 +1400,11 @@ def _train_tts_encoder(
13981400 # (offline-reference style); otherwise alternate_batching (pure audio / pure text batches).
13991401 ** ({} if single_stream else {"torch_batching" : functools .partial (alternate_batching , asr_key = in_key )}),
14001402 # custom step; no TrainDef needed
1401- "train_step" : aed_glowtts_single_stream_train_step if single_stream else aed_glowtts_train_step ,
1403+ "train_step" : (
1404+ (aed_pseudo_enc_single_stream_train_step if pseudo_speech_enc else aed_glowtts_single_stream_train_step )
1405+ if single_stream
1406+ else aed_glowtts_train_step
1407+ ),
14021408 "learning_rate_control_error_measure" : "ce" , # set explicitly since there is no TrainDef
14031409 "speed_pert_discrete_values" : [0.7 , 0.8 , 0.9 , 1.0 , 1.1 ],
14041410 "aux_loss_layers" : [4 , 10 , 16 ],
@@ -2089,6 +2095,213 @@ def aed_glowtts_single_stream_train_step(*, model: Model, extern_data, **_kwargs
20892095 frame_error .mark_as_loss (name = f"fer{ postfix } " , as_error = True )
20902096
20912097
2098+ def aed_pseudo_enc_single_stream_train_step (* , model : Model , extern_data , ** _kwargs_unused ):
2099+ """Custom RETURNN train_step: single-stream AED+CTC with the pseudo-speech-encoder
2100+ (real audio + pseudo-enc text seqs mixed in one batch).
2101+
2102+ Like aed_glowtts_single_stream_train_step, but the text branch enters the encoder
2103+ at layer pseudo_enc_start_layer (encoder model space, enc frame rate) instead of at the waveform:
2104+ the pseudo features are computed on the FULL batch
2105+ (audio rows have empty phoneme seqs -> empty rows; a plain embedding lookup, so this is free)
2106+ and get the text-side SpecAugment + input dropout;
2107+ the audio rows run the front-end + Conformer layers [0, start_layer)
2108+ and are scattered in at the layer boundary (rf.nested handles the ragged frame dims);
2109+ Conformer layers [start_layer, ...) + decoder + ONE loss set run once on the merged batch.
2110+ Aux CTC tapped below start_layer (ctc_4 at start_layer=4) is computed on the audio rows only
2111+ (same coverage as dual-stream, where the text branch skips those aux losses).
2112+ """
2113+ from returnn .config import get_global_config
2114+ from returnn .util .collect_outputs_dict import CollectOutputsDict
2115+
2116+ config = get_global_config () # noqa
2117+ assert config .bool ("pseudo_speech_enc" , False )
2118+ start_layer = config .int ("pseudo_enc_start_layer" , - 1 )
2119+ assert start_layer >= 0 , "pseudo-enc single-stream: layer-split injection only"
2120+ assert config .typed_value ("pseudo_enc_units" , "phonemes" ) == "phonemes" , "pseudo-enc single-stream: phonemes only"
2121+ data = extern_data [config .typed_value ("default_input" )]
2122+ data_spatial_dim = data .get_time_dim_tag ()
2123+ targets = extern_data [config .typed_value ("target" )]
2124+ targets_spatial_dim = targets .get_time_dim_tag ()
2125+ phonemes = extern_data [PHONEMES_DATA_KEY ]
2126+ phonemes_spatial_dim = phonemes .get_time_dim_tag ()
2127+
2128+ aux_loss_layers = config .typed_value ("aux_loss_layers" ) or ()
2129+ aux_loss_scales = config .typed_value ("aux_loss_scales" , [1.0 ] * len (aux_loss_layers ))
2130+ aed_loss_scale = config .float ("aed_loss_scale" , 1.0 )
2131+ dec_aux_loss_layers = config .typed_value ("dec_aux_loss_layers" ) or ()
2132+ dec_aux_loss_scales = config .typed_value ("dec_aux_loss_scales" , [1.0 ] * len (dec_aux_loss_layers ))
2133+ use_normalized_loss = config .typed_value ("use_normalized_loss" , True )
2134+ if isinstance (use_normalized_loss , bool ):
2135+ use_normalized_loss = "frames" if use_normalized_loss else "none"
2136+ assert use_normalized_loss in ("none" , "frames" ), f"single_stream: unsupported { use_normalized_loss !r} "
2137+ normed = {"none" : False , "frames" : True }[use_normalized_loss ]
2138+ label_smoothing = config .float ("label_smoothing" , 0.1 )
2139+ pseudo_specaug_width = config .typed_value ("pseudo_enc_specaug_max_width" , None )
2140+
2141+ if data .feature_dim and data .feature_dim .dimension == 1 :
2142+ data = rf .squeeze (data , axis = data .feature_dim )
2143+ (batch_dim ,) = data .remaining_dims (data_spatial_dim )
2144+ # per-seq split: text seqs carry empty audio (size 0); real-audio seqs carry empty phonemes.
2145+ text_mask_cpu = data_spatial_dim .get_size_tensor () <= 0
2146+ audio_mask_cpu = rf .logical_not (text_mask_cpu )
2147+ audio_mask = rf .copy_to_device (audio_mask_cpu , data .device )
2148+ num_text = int (text_mask_cpu .raw_tensor .sum ())
2149+ num_total = int (batch_dim .get_dim_value ())
2150+
2151+ if config .bool ("use_eos_postfix" , False ):
2152+ ctc_targets , (ctc_targets_spatial_dim ,) = rf .pad (
2153+ targets , axes = [targets_spatial_dim ], padding = [(0 , 1 )], value = model .eos_idx
2154+ )
2155+ else :
2156+ ctc_targets , ctc_targets_spatial_dim = targets , targets_spatial_dim
2157+
2158+ n_layers = len (model .encoder .layers )
2159+ # aux heads tapped below the merge boundary (collected key < start_layer): audio rows only.
2160+ aux_below = [i for i in aux_loss_layers if i - 1 < start_layer ]
2161+ collected = CollectOutputsDict (allowed_key_patterns = [str (i - 1 ) for i in aux_loss_layers ])
2162+ aux_below_out = {}
2163+ audio_ct = audio_ct_sp = aux_below_sp = None
2164+ if num_text == 0 :
2165+ # pure real-audio batch: the standard full encode (all aux keys collected).
2166+ enc , enc_spatial_dim = model .encode (data , in_spatial_dim = data_spatial_dim , collected_outputs = collected )
2167+ elif num_text == num_total :
2168+ # pure text batch: the plain layer-split path (aux below start_layer skipped, as in dual-stream).
2169+ feats , feats_spatial_dim = model .pseudo_enc (phonemes , spatial_dim = phonemes_spatial_dim )
2170+ enc_raw , enc_spatial_dim = model .encode_from_enc_space (
2171+ feats ,
2172+ spatial_dim = feats_spatial_dim ,
2173+ start_layer = start_layer ,
2174+ collected_outputs = collected ,
2175+ specaugment_max_spatial_dims = pseudo_specaug_width ,
2176+ )
2177+ enc = model .decoder .transform_encoder (enc_raw , axis = enc_spatial_dim )
2178+ else :
2179+ # text side: pseudo features on the FULL batch (audio rows are empty), then the text-side
2180+ # SpecAugment (scaled width) + input dropout; start_layer=n_layers runs no Conformer layer.
2181+ feats_t , feats_t_spatial_dim = model .pseudo_enc (phonemes , spatial_dim = phonemes_spatial_dim )
2182+ feats_t , _ = model .encode_from_enc_space (
2183+ feats_t ,
2184+ spatial_dim = feats_t_spatial_dim ,
2185+ start_layer = n_layers ,
2186+ specaugment_max_spatial_dims = pseudo_specaug_width ,
2187+ )
2188+ # audio side: select the real-audio rows, front-end + Conformer layers [0, start_layer).
2189+ (wave_a , wave_a_spatial_dim ), audio_bdim , sel_map_a = rf .nested .masked_select_nested (
2190+ (data , data_spatial_dim ), mask = audio_mask , mask_cpu = audio_mask_cpu , dims = [batch_dim ]
2191+ )
2192+ coll_a = CollectOutputsDict (allowed_key_patterns = [str (i - 1 ) for i in aux_below ])
2193+ feats_a , feats_a_spatial_dim = model .encode_no_transform (
2194+ wave_a , in_spatial_dim = wave_a_spatial_dim , collected_outputs = coll_a , end_layer = start_layer
2195+ )
2196+ if aux_below :
2197+ (audio_ct , audio_ct_sp ), _ , _ = rf .nested .masked_select_nested (
2198+ (ctc_targets , ctc_targets_spatial_dim ), mask = audio_mask , mask_cpu = audio_mask_cpu , dims = [batch_dim ]
2199+ )
2200+ aux_below_out = {i : coll_a [str (i - 1 )] for i in aux_below }
2201+ aux_below_sp = feats_a_spatial_dim
2202+ # merge at the layer boundary: scatter the audio rows into the full-batch pseudo features.
2203+ if feats_t .dtype != feats_a .dtype :
2204+ feats_t = rf .cast (feats_t , feats_a .dtype )
2205+ x , x_spatial_dim = rf .nested .masked_scatter_nested (
2206+ (feats_a , feats_a_spatial_dim ),
2207+ (feats_t , feats_t_spatial_dim ),
2208+ mask = audio_mask ,
2209+ mask_cpu = audio_mask_cpu ,
2210+ dims = [batch_dim ],
2211+ in_dim = audio_bdim ,
2212+ masked_select_dim_map = sel_map_a ,
2213+ )
2214+ enc_raw , enc_spatial_dim = model .encode_from_enc_space (
2215+ x ,
2216+ spatial_dim = x_spatial_dim ,
2217+ start_layer = start_layer ,
2218+ collected_outputs = collected ,
2219+ apply_specaugment = False ,
2220+ apply_input_dropout = False ,
2221+ )
2222+ enc = model .decoder .transform_encoder (enc_raw , axis = enc_spatial_dim )
2223+
2224+ for i , layer_idx in enumerate (aux_loss_layers ):
2225+ if layer_idx > n_layers :
2226+ continue
2227+ key = str (layer_idx - 1 )
2228+ if key in collected :
2229+ aux_out , aux_sp = collected [key ], enc_spatial_dim
2230+ aux_tgt , aux_tgt_sp = ctc_targets , ctc_targets_spatial_dim
2231+ elif layer_idx in aux_below_out :
2232+ # tapped below the merge: audio rows only (dual-stream text skips these too).
2233+ aux_out , aux_sp = aux_below_out [layer_idx ], aux_below_sp
2234+ aux_tgt , aux_tgt_sp = audio_ct , audio_ct_sp
2235+ else :
2236+ continue # pure-text batch: no signal below the injection layer
2237+ aux_logits = getattr (model , f"enc_aux_logits_{ layer_idx } " )(aux_out )
2238+ aux_ctc_log_probs = rf .log_softmax (aux_logits , axis = model .wb_target_dim )
2239+ aux_loss = rf .ctc_loss (
2240+ logits = aux_ctc_log_probs ,
2241+ logits_normalized = True ,
2242+ targets = aux_tgt ,
2243+ input_spatial_dim = aux_sp ,
2244+ targets_spatial_dim = aux_tgt_sp ,
2245+ blank_index = model .blank_idx ,
2246+ )
2247+ aux_loss .mark_as_loss (
2248+ f"ctc_{ layer_idx } " ,
2249+ scale = aux_loss_scales [i ],
2250+ custom_inv_norm_factor = aux_tgt_sp .get_size_tensor (),
2251+ use_normalized_loss = normed ,
2252+ )
2253+
2254+ # AED CE + FER (single stream), identical to aed_glowtts_single_stream_train_step.
2255+ batch_dims = targets .remaining_dims (targets_spatial_dim )
2256+ input_labels , (targets_w_eos_spatial_dim ,) = rf .pad (
2257+ targets , axes = [targets_spatial_dim ], padding = [(1 , 0 )], value = model .bos_idx
2258+ )
2259+ targets_w_eos , _ = rf .pad (
2260+ targets ,
2261+ axes = [targets_spatial_dim ],
2262+ padding = [(0 , 1 )],
2263+ value = model .eos_idx ,
2264+ out_dims = [targets_w_eos_spatial_dim ],
2265+ )
2266+ dec_collected = CollectOutputsDict (allowed_key_patterns = [str (i - 1 ) for i in dec_aux_loss_layers ])
2267+ logits , _ = model .decoder (
2268+ input_labels ,
2269+ spatial_dim = targets_w_eos_spatial_dim ,
2270+ encoder = enc ,
2271+ state = model .decoder .default_initial_state (batch_dims = batch_dims ),
2272+ collected_outputs = dec_collected ,
2273+ )
2274+ dec_aux_logits = {}
2275+ for layer_idx in dec_aux_loss_layers :
2276+ norm = getattr (model , f"dec_aux_final_layer_norm_{ layer_idx } " )
2277+ linear = getattr (model , f"dec_aux_logits_{ layer_idx } " )
2278+ dec_aux_logits [layer_idx ] = linear (norm (dec_collected [str (layer_idx - 1 )]))
2279+
2280+ targets_packed , pack_dim = rf .pack_padded (
2281+ targets_w_eos , dims = batch_dims + [targets_w_eos_spatial_dim ], enforce_sorted = False
2282+ )
2283+ for postfix , scale , logits_ in [("" , aed_loss_scale , logits )] + [
2284+ (f"_{ k } " , dec_aux_loss_scales [i ], dec_aux_logits [k ]) for i , k in enumerate (dec_aux_loss_layers )
2285+ ]:
2286+ logits_packed , _ = rf .pack_padded (
2287+ logits_ , dims = batch_dims + [targets_w_eos_spatial_dim ], enforce_sorted = False , out_dim = pack_dim
2288+ )
2289+ if not model .out_eos_separated :
2290+ log_prob = rf .log_softmax (logits_packed , axis = model .target_dim )
2291+ else :
2292+ log_prob = _aed .log_probs_with_eos_separated (
2293+ logits_packed , target_dim = model .target_dim , eos_idx = model .eos_idx
2294+ )
2295+ log_prob = rf .label_smoothed_log_prob_gradient (log_prob , label_smoothing , axis = model .target_dim )
2296+ loss = rf .cross_entropy (
2297+ target = targets_packed , estimated = log_prob , estimated_type = "log-probs" , axis = model .target_dim
2298+ )
2299+ loss .mark_as_loss (f"ce{ postfix } " , scale = scale , use_normalized_loss = normed )
2300+ best = rf .reduce_argmax (log_prob , axis = model .target_dim )
2301+ frame_error = best != targets_packed
2302+ frame_error .mark_as_loss (name = f"fer{ postfix } " , as_error = True )
2303+
2304+
20922305_glowtts_text_tok_cache = None
20932306
20942307
0 commit comments